Przeglądaj źródła

Merge stable branch

Antoine du Hamel 1 rok temu
rodzic
commit
6f761fffd5
73 zmienionych plików z 13404 dodań i 118 usunięć
  1. 7 0
      .eslintrc.js
  2. 9 0
      .github/dependabot.yml
  3. 2 2
      .github/workflows/bundlers.yml
  4. 4 4
      .github/workflows/ci.yml
  5. 2 2
      .github/workflows/companion-deploy.yml
  6. 2 2
      .github/workflows/companion.yml
  7. 6 6
      .github/workflows/e2e.yml
  8. 25 4
      .github/workflows/linters.yml
  9. 2 2
      .github/workflows/lockfile_check.yml
  10. 2 2
      .github/workflows/manual-cdn.yml
  11. 2 2
      .github/workflows/release-candidate.yml
  12. 4 4
      .github/workflows/release.yml
  13. 7 0
      .prettierrc.js
  14. 22 0
      CHANGELOG.md
  15. 79 80
      README.md
  16. 3 0
      docs/README.md
  17. 935 0
      docs/companion.md
  18. 125 0
      docs/compressor.mdx
  19. 181 0
      docs/form.mdx
  20. 4 0
      docs/framework-integrations/_category_.json
  21. 108 0
      docs/framework-integrations/angular.mdx
  22. 228 0
      docs/framework-integrations/react.mdx
  23. 70 0
      docs/framework-integrations/svelte.mdx
  24. 73 0
      docs/framework-integrations/vue.mdx
  25. 150 0
      docs/golden-retriever.mdx
  26. 4 0
      docs/guides/_category_.json
  27. 62 0
      docs/guides/browser-support.mdx
  28. 382 0
      docs/guides/building-plugins.md
  29. 11 0
      docs/guides/building-your-own-ui-with-uppy.md
  30. 93 0
      docs/guides/choosing-uploader.md
  31. 136 0
      docs/guides/custom-stores.md
  32. 651 0
      docs/guides/migration-guides.md
  33. 1562 0
      docs/locales.mdx
  34. 4 0
      docs/presets/_category_.json
  35. 141 0
      docs/presets/remote-sources.mdx
  36. 58 0
      docs/quick-start.mdx
  37. 4 0
      docs/sources/_category_.json
  38. 134 0
      docs/sources/audio.mdx
  39. 5 0
      docs/sources/companion-plugins/_category_.json
  40. 189 0
      docs/sources/companion-plugins/box.mdx
  41. 188 0
      docs/sources/companion-plugins/dropbox.mdx
  42. 187 0
      docs/sources/companion-plugins/facebook.mdx
  43. 191 0
      docs/sources/companion-plugins/google-drive.mdx
  44. 181 0
      docs/sources/companion-plugins/instagram.mdx
  45. 181 0
      docs/sources/companion-plugins/onedrive.mdx
  46. 166 0
      docs/sources/companion-plugins/unsplash.mdx
  47. 149 0
      docs/sources/companion-plugins/url.mdx
  48. 160 0
      docs/sources/companion-plugins/zoom.mdx
  49. 166 0
      docs/sources/file-input.mdx
  50. 151 0
      docs/sources/screen-capture.mdx
  51. 253 0
      docs/sources/webcam.mdx
  52. 4 0
      docs/uploader/_category_.json
  53. 522 0
      docs/uploader/aws-s3-multipart.mdx
  54. 463 0
      docs/uploader/aws-s3.mdx
  55. 684 0
      docs/uploader/transloadit.mdx
  56. 308 0
      docs/uploader/tus.mdx
  57. 399 0
      docs/uploader/xhr.mdx
  58. 1628 0
      docs/uppy-core.mdx
  59. 4 0
      docs/user-interfaces/_category_.json
  60. 753 0
      docs/user-interfaces/dashboard.mdx
  61. 149 0
      docs/user-interfaces/drag-drop.mdx
  62. 5 0
      docs/user-interfaces/elements/_category_.json
  63. 113 0
      docs/user-interfaces/elements/drop-target.mdx
  64. 179 0
      docs/user-interfaces/elements/image-editor.mdx
  65. 94 0
      docs/user-interfaces/elements/informer.mdx
  66. 91 0
      docs/user-interfaces/elements/progress-bar.mdx
  67. 201 0
      docs/user-interfaces/elements/status-bar.mdx
  68. 168 0
      docs/user-interfaces/elements/thumbnail-generator.mdx
  69. 18 8
      examples/custom-provider/client/MyCustomProvider.jsx
  70. 7 0
      packages/@uppy/dashboard/CHANGELOG.md
  71. 1 0
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx
  72. 7 0
      packages/@uppy/utils/CHANGELOG.md
  73. 145 0
      packages/@uppy/utils/src/fetcher.ts

+ 7 - 0
.eslintrc.js

@@ -413,8 +413,15 @@ module.exports = {
       files: ['**/*.md', '*.md'],
       processor: 'markdown/markdown',
     },
+    {
+      files: ['docs/**/*.md/*.js'],
+      parserOptions: {
+        sourceType: 'module',
+      },
+    },
     {
       files: ['**/*.md/*.js', '**/*.md/*.javascript'],
+      excludedFiles: ["docs/**/*"],
       parserOptions: {
         sourceType: 'module',
       },

+ 9 - 0
.github/dependabot.yml

@@ -0,0 +1,9 @@
+# Basic set up for three package managers
+
+version: 2
+updates:
+  # Maintain dependencies for GitHub Actions
+  - package-ecosystem: 'github-actions'
+    directory: '/'
+    schedule:
+      interval: 'weekly'

+ 2 - 2
.github/workflows/bundlers.yml

@@ -29,7 +29,7 @@ jobs:
         id: yarn-cache-dir-path
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -37,7 +37,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies

+ 4 - 4
.github/workflows/ci.yml

@@ -43,7 +43,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -51,7 +51,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: ${{matrix.node-version}}
       - name: Install dependencies
@@ -76,7 +76,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -84,7 +84,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies

+ 2 - 2
.github/workflows/companion-deploy.yml

@@ -49,13 +49,13 @@ jobs:
         uses: actions/checkout@v3
       - name: Docker meta
         id: docker_meta
-        uses: docker/metadata-action@v4
+        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
         with:
           images: transloadit/companion
           tags: |
             type=edge
             type=raw,value=latest,enable=false
-      - uses: docker/setup-qemu-action@v2
+      - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
       - uses: docker/setup-buildx-action@v2
       - name: Log in to DockerHub
         uses: docker/login-action@v2

+ 2 - 2
.github/workflows/companion.yml

@@ -32,7 +32,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -40,7 +40,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: ${{matrix.node-version}}
       - name: Install dependencies

+ 6 - 6
.github/workflows/e2e.yml

@@ -70,7 +70,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -78,7 +78,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies
@@ -230,7 +230,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -241,17 +241,17 @@ jobs:
       - name: Create cache folder for Cypress
         id: cypress-cache-dir-path
         run: echo "dir=$(mktemp -d)" >> $GITHUB_OUTPUT
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         with:
           path: ${{ steps.cypress-cache-dir-path.outputs.dir }}
           key: ${{ runner.os }}-cypress
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
 
       - name: Start Redis
-        uses: supercharge/redis-github-action@1.4.0
+        uses: supercharge/redis-github-action@ea9b21c6ecece47bd99595c532e481390ea0f044 # 1.8.0
         with:
           redis-version: 7
 

+ 25 - 4
.github/workflows/linters.yml

@@ -30,7 +30,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -38,7 +38,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies
@@ -62,7 +62,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -70,10 +70,31 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies
         run: corepack yarn workspaces focus @uppy-dev/build
       - name: Run linter
         run: corepack yarn run lint:markdown
+
+  lint_docs:
+    name: Lint Docs
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Uppy.io sources
+        uses: actions/checkout@v3
+        with:
+          repository: transloadit/uppy.io
+      - run: rm -rf docs # the other PR has not landed
+      - name: Checkout docs
+        uses: actions/checkout@v3
+        with:
+          path: uppy
+      - run: mv uppy /tmp/uppy && ln -s /tmp/uppy/docs docs
+      - name: Install dependencies
+        run: corepack yarn --immutable
+      - name: Lint files
+        run: corepack yarn lint
+      - name: Test build website
+        run: corepack yarn build

+ 2 - 2
.github/workflows/lockfile_check.yml

@@ -26,7 +26,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -34,7 +34,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies

+ 2 - 2
.github/workflows/manual-cdn.yml

@@ -21,7 +21,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -29,7 +29,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies

+ 2 - 2
.github/workflows/release-candidate.yml

@@ -22,7 +22,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -30,7 +30,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies

+ 4 - 4
.github/workflows/release.yml

@@ -27,7 +27,7 @@ jobs:
         run:
           echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
         with:
           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -35,7 +35,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-yarn-
       - name: Install Node.js
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: lts/*
       - name: Install dependencies
@@ -145,7 +145,7 @@ jobs:
         uses: actions/checkout@v3
       - name: Docker meta
         id: docker_meta
-        uses: docker/metadata-action@v4
+        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
         with:
           images: transloadit/companion
           tags: |
@@ -153,7 +153,7 @@ jobs:
             type=semver,pattern={{version}},value=${{ needs.release.outputs.companionWasReleased }}
             # set latest tag for default branch
             type=raw,value=latest,enable=true
-      - uses: docker/setup-qemu-action@v2
+      - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
       - uses: docker/setup-buildx-action@v2
       - name: Log in to DockerHub
         uses: docker/login-action@v2

+ 7 - 0
.prettierrc.js

@@ -12,5 +12,12 @@ module.exports = {
         semi: true,
       },
     },
+    {
+      files: "docs/**",
+      options: {
+        semi: true,
+        useTabs: true,
+      }
+    }
   ],
 }

+ 22 - 0
CHANGELOG.md

@@ -165,6 +165,28 @@ Released: 2024-03-28
 - meta: prepare release workflow for beta versions (Antoine du Hamel)
 
 
+## 3.24.3
+
+Released: 2024-04-16
+
+| Package         | Version | Package         | Version |
+| --------------- | ------- | --------------- | ------- |
+| @uppy/dashboard |   3.8.1 | uppy            |  3.24.3 |
+| @uppy/utils     |   5.8.0 |                 |         |
+
+- docs: add back markdown files (Antoine du Hamel / #5064)
+- meta: fix custom provider example (Merlijn Vos / #5079)
+- @uppy/utils: add fetcher (Merlijn Vos / #5073)
+- meta: Fix prettier (Murderlon)
+- @uppy/dashboard: add missing `x-zip-compress` archive type (Younes / #5081)
+- meta: Bump docker/metadata-action from 4 to 5 (dependabot[bot] / #5086)
+- meta: Bump actions/setup-node from 3 to 4 (dependabot[bot] / #5087)
+- meta: Bump docker/setup-qemu-action from 2 to 3 (dependabot[bot] / #5089)
+- meta: bump supercharge/redis-github-action from 1.4.0 to 1.8.0 (dependabot[bot] / #5090)
+- meta: bump actions/cache from 3 to 4 (dependabot[bot] / #5088)
+- meta: add `dependabot.yml` to keep GHA up-to-date (Antoine du Hamel / #5083)
+
+
 ## 3.24.2
 
 Released: 2024-04-15

+ 79 - 80
README.md

@@ -207,9 +207,9 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [Murderlon](https://github.com/Murderlon) |[AJvanLoon](https://github.com/AJvanLoon) |[nqst](https://github.com/nqst) |[mifi](https://github.com/mifi) |[github-actions\[bot\]](https://github.com/apps/github-actions) |[lakesare](https://github.com/lakesare) |
 
-[<img alt="kiloreux" src="https://avatars.githubusercontent.com/u/6282557?v=4&s=117" width="117">](https://github.com/kiloreux) |[<img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4&s=117" width="117">](https://github.com/apps/dependabot) |[<img alt="samuelayo" src="https://avatars.githubusercontent.com/u/14964486?v=4&s=117" width="117">](https://github.com/samuelayo) |[<img alt="sadovnychyi" src="https://avatars.githubusercontent.com/u/193864?v=4&s=117" width="117">](https://github.com/sadovnychyi) |[<img alt="richardwillars" src="https://avatars.githubusercontent.com/u/291004?v=4&s=117" width="117">](https://github.com/richardwillars) |[<img alt="ajkachnic" src="https://avatars.githubusercontent.com/u/44317699?v=4&s=117" width="117">](https://github.com/ajkachnic) |
+[<img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4&s=117" width="117">](https://github.com/apps/dependabot) |[<img alt="kiloreux" src="https://avatars.githubusercontent.com/u/6282557?v=4&s=117" width="117">](https://github.com/kiloreux) |[<img alt="samuelayo" src="https://avatars.githubusercontent.com/u/14964486?v=4&s=117" width="117">](https://github.com/samuelayo) |[<img alt="sadovnychyi" src="https://avatars.githubusercontent.com/u/193864?v=4&s=117" width="117">](https://github.com/sadovnychyi) |[<img alt="richardwillars" src="https://avatars.githubusercontent.com/u/291004?v=4&s=117" width="117">](https://github.com/richardwillars) |[<img alt="ajkachnic" src="https://avatars.githubusercontent.com/u/44317699?v=4&s=117" width="117">](https://github.com/ajkachnic) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[kiloreux](https://github.com/kiloreux) |[dependabot\[bot\]](https://github.com/apps/dependabot) |[samuelayo](https://github.com/samuelayo) |[sadovnychyi](https://github.com/sadovnychyi) |[richardwillars](https://github.com/richardwillars) |[ajkachnic](https://github.com/ajkachnic) |
+[dependabot\[bot\]](https://github.com/apps/dependabot) |[kiloreux](https://github.com/kiloreux) |[samuelayo](https://github.com/samuelayo) |[sadovnychyi](https://github.com/sadovnychyi) |[richardwillars](https://github.com/richardwillars) |[ajkachnic](https://github.com/ajkachnic) |
 
 [<img alt="zcallan" src="https://avatars.githubusercontent.com/u/13760738?v=4&s=117" width="117">](https://github.com/zcallan) |[<img alt="YukeshShr" src="https://avatars.githubusercontent.com/u/71844521?v=4&s=117" width="117">](https://github.com/YukeshShr) |[<img alt="janko" src="https://avatars.githubusercontent.com/u/795488?v=4&s=117" width="117">](https://github.com/janko) |[<img alt="oliverpool" src="https://avatars.githubusercontent.com/u/3864879?v=4&s=117" width="117">](https://github.com/oliverpool) |[<img alt="Botz" src="https://avatars.githubusercontent.com/u/2706678?v=4&s=117" width="117">](https://github.com/Botz) |[<img alt="mcallistertyler" src="https://avatars.githubusercontent.com/u/14939210?v=4&s=117" width="117">](https://github.com/mcallistertyler) |
 :---: |:---: |:---: |:---: |:---: |:---: |
@@ -275,53 +275,53 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [dogrocker](https://github.com/dogrocker) |[jedwood](https://github.com/jedwood) |[jasonbosco](https://github.com/jasonbosco) |[ghasrfakhri](https://github.com/ghasrfakhri) |[geertclerx](https://github.com/geertclerx) |[frobinsonj](https://github.com/frobinsonj) |
 
-[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |
+[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |[raulibanez](https://github.com/raulibanez) |[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |
+[neuronet77](https://github.com/neuronet77) |[rossng](https://github.com/rossng) |[scherroman](https://github.com/scherroman) |[robwilson1](https://github.com/robwilson1) |[SxDx](https://github.com/SxDx) |[refo](https://github.com/refo) |
 
-[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |
+[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[scherroman](https://github.com/scherroman) |[neuronet77](https://github.com/neuronet77) |[Pzoco](https://github.com/Pzoco) |[ppadmavilasom](https://github.com/ppadmavilasom) |[phillipalexander](https://github.com/phillipalexander) |[pmusaraj](https://github.com/pmusaraj) |
+[raulibanez](https://github.com/raulibanez) |[luarmr](https://github.com/luarmr) |[eman8519](https://github.com/eman8519) |[Pzoco](https://github.com/Pzoco) |[ppadmavilasom](https://github.com/ppadmavilasom) |[phillipalexander](https://github.com/phillipalexander) |
 
-[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |
+[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[pedrofs](https://github.com/pedrofs) |[plneto](https://github.com/plneto) |[patricklindsay](https://github.com/patricklindsay) |[pascalwengerter](https://github.com/pascalwengerter) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[taj](https://github.com/taj) |
+[pmusaraj](https://github.com/pmusaraj) |[pedrofs](https://github.com/pedrofs) |[plneto](https://github.com/plneto) |[patricklindsay](https://github.com/patricklindsay) |[pascalwengerter](https://github.com/pascalwengerter) |[Tashows](https://github.com/Tashows) |
 
-[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |
+[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[strayer](https://github.com/strayer) |[sjauld](https://github.com/sjauld) |[steverob](https://github.com/steverob) |[amaitu](https://github.com/amaitu) |[quigebo](https://github.com/quigebo) |[waptik](https://github.com/waptik) |
+[taj](https://github.com/taj) |[strayer](https://github.com/strayer) |[sjauld](https://github.com/sjauld) |[steverob](https://github.com/steverob) |[amaitu](https://github.com/amaitu) |[quigebo](https://github.com/quigebo) |
 
-[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |
+[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[SpazzMarticus](https://github.com/SpazzMarticus) |[szh](https://github.com/szh) |[sergei-zelinsky](https://github.com/sergei-zelinsky) |[sebasegovia01](https://github.com/sebasegovia01) |[sdebacker](https://github.com/sdebacker) |[samuelcolburn](https://github.com/samuelcolburn) |
+[waptik](https://github.com/waptik) |[SpazzMarticus](https://github.com/SpazzMarticus) |[szh](https://github.com/szh) |[sergei-zelinsky](https://github.com/sergei-zelinsky) |[sebasegovia01](https://github.com/sebasegovia01) |[sdebacker](https://github.com/sdebacker) |
 
-[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |
+[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[fortunto2](https://github.com/fortunto2) |[GNURub](https://github.com/GNURub) |[rart](https://github.com/rart) |[rossng](https://github.com/rossng) |[mkopinsky](https://github.com/mkopinsky) |[mhulet](https://github.com/mhulet) |
+[samuelcolburn](https://github.com/samuelcolburn) |[fortunto2](https://github.com/fortunto2) |[GNURub](https://github.com/GNURub) |[rart](https://github.com/rart) |[ken-kuro](https://github.com/ken-kuro) |[mkopinsky](https://github.com/mkopinsky) |
 
-[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |
+[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[hrsh](https://github.com/hrsh) |[mauricioribeiro](https://github.com/mauricioribeiro) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mjesuele](https://github.com/mjesuele) |[mattfik](https://github.com/mattfik) |[mateuscruz](https://github.com/mateuscruz) |
+[mhulet](https://github.com/mhulet) |[hrsh](https://github.com/hrsh) |[mauricioribeiro](https://github.com/mauricioribeiro) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mjesuele](https://github.com/mjesuele) |[mattfik](https://github.com/mattfik) |
 
-[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |
+[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[masumulu28](https://github.com/masumulu28) |[masaok](https://github.com/masaok) |[martin-brennan](https://github.com/martin-brennan) |[marcusforsberg](https://github.com/marcusforsberg) |[marcosthejew](https://github.com/marcosthejew) |[mperrando](https://github.com/mperrando) |
+[mateuscruz](https://github.com/mateuscruz) |[masumulu28](https://github.com/masumulu28) |[masaok](https://github.com/masaok) |[martin-brennan](https://github.com/martin-brennan) |[marcusforsberg](https://github.com/marcusforsberg) |[marcosthejew](https://github.com/marcosthejew) |
 
-[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="Cruaier" src="https://avatars.githubusercontent.com/u/5204940?v=4&s=117" width="117">](https://github.com/Cruaier) |[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |
+[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[onhate](https://github.com/onhate) |[marc-mabe](https://github.com/marc-mabe) |[Cruaier](https://github.com/Cruaier) |[cryptic022](https://github.com/cryptic022) |[Ozodbek1405](https://github.com/Ozodbek1405) |[leftdevel](https://github.com/leftdevel) |
+[mperrando](https://github.com/mperrando) |[onhate](https://github.com/onhate) |[marc-mabe](https://github.com/marc-mabe) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[cryptic022](https://github.com/cryptic022) |[Ozodbek1405](https://github.com/Ozodbek1405) |
 
-[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |
+[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[nil1511](https://github.com/nil1511) |[coreprocess](https://github.com/coreprocess) |[nicojones](https://github.com/nicojones) |[trungcva10a6tn](https://github.com/trungcva10a6tn) |[naveed-ahmad](https://github.com/naveed-ahmad) |[pleasespammelater](https://github.com/pleasespammelater) |
+[leftdevel](https://github.com/leftdevel) |[nil1511](https://github.com/nil1511) |[coreprocess](https://github.com/coreprocess) |[nicojones](https://github.com/nicojones) |[trungcva10a6tn](https://github.com/trungcva10a6tn) |[naveed-ahmad](https://github.com/naveed-ahmad) |
 
-[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |
+[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[navruzm](https://github.com/navruzm) |[mogzol](https://github.com/mogzol) |[shahimclt](https://github.com/shahimclt) |[mnafees](https://github.com/mnafees) |[boudra](https://github.com/boudra) |
+[pleasespammelater](https://github.com/pleasespammelater) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[navruzm](https://github.com/navruzm) |[mogzol](https://github.com/mogzol) |[shahimclt](https://github.com/shahimclt) |[mnafees](https://github.com/mnafees) |
 
-[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |
+[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[achmiral](https://github.com/achmiral) |[ken-kuro](https://github.com/ken-kuro) |[mosi-kha](https://github.com/mosi-kha) |[maddy-jo](https://github.com/maddy-jo) |[mdxiaohu](https://github.com/mdxiaohu) |[magumbo](https://github.com/magumbo) |
+[boudra](https://github.com/boudra) |[achmiral](https://github.com/achmiral) |[mosi-kha](https://github.com/mosi-kha) |[maddy-jo](https://github.com/maddy-jo) |[mdxiaohu](https://github.com/mdxiaohu) |[magumbo](https://github.com/magumbo) |
 
 [<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |[<img alt="jyoungblood" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/jyoungblood) |
 :---: |:---: |:---: |:---: |:---: |:---: |
@@ -331,9 +331,9 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [green-mike](https://github.com/green-mike) |[gaelicwinter](https://github.com/gaelicwinter) |[frederikhors](https://github.com/frederikhors) |[franckl](https://github.com/franckl) |[fingul](https://github.com/fingul) |[elliotsayes](https://github.com/elliotsayes) |
 
-[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |[<img alt="zanzlender" src="https://avatars.githubusercontent.com/u/44570474?v=4&s=117" width="117">](https://github.com/zanzlender) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |
+[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |[<img alt="zanzlender" src="https://avatars.githubusercontent.com/u/44570474?v=4&s=117" width="117">](https://github.com/zanzlender) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[xhocquet](https://github.com/xhocquet) |[JimmyLv](https://github.com/JimmyLv) |[zanzlender](https://github.com/zanzlender) |[olitomas](https://github.com/olitomas) |[yoann-hellopret](https://github.com/yoann-hellopret) |[vedran555](https://github.com/vedran555) |
+[YehudaKremer](https://github.com/YehudaKremer) |[JimmyLv](https://github.com/JimmyLv) |[zanzlender](https://github.com/zanzlender) |[olitomas](https://github.com/olitomas) |[yoann-hellopret](https://github.com/yoann-hellopret) |[vedran555](https://github.com/vedran555) |
 
 [<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |
 :---: |:---: |:---: |:---: |:---: |:---: |
@@ -343,9 +343,9 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [rlebosse](https://github.com/rlebosse) |[rhymes](https://github.com/rhymes) |[luntta](https://github.com/luntta) |[phil714](https://github.com/phil714) |[ordago](https://github.com/ordago) |[odselsevier](https://github.com/odselsevier) |
 
-[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |
+[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ninesalt](https://github.com/ninesalt) |[willycamargo](https://github.com/willycamargo) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[dwnste](https://github.com/dwnste) |[nagyv](https://github.com/nagyv) |[stiig](https://github.com/stiig) |
+[ninesalt](https://github.com/ninesalt) |[xhocquet](https://github.com/xhocquet) |[willycamargo](https://github.com/willycamargo) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[dwnste](https://github.com/dwnste) |[nagyv](https://github.com/nagyv) |
 
 [<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |
 :---: |:---: |:---: |:---: |:---: |:---: |
@@ -363,105 +363,105 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [canvasbh](https://github.com/canvasbh) |[c0b41](https://github.com/c0b41) |[avalla](https://github.com/avalla) |[arggh](https://github.com/arggh) |[alfatv](https://github.com/alfatv) |[agreene-coursera](https://github.com/agreene-coursera) |
 
-[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |
+[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |[<img alt="yafkari" src="https://avatars.githubusercontent.com/u/41365655?v=4&s=117" width="117">](https://github.com/yafkari) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[aduh95-test-account](https://github.com/aduh95-test-account) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[zackbloom](https://github.com/zackbloom) |[zlawson-ut](https://github.com/zlawson-ut) |[zachconner](https://github.com/zachconner) |[YehudaKremer](https://github.com/YehudaKremer) |
+[aduh95-test-account](https://github.com/aduh95-test-account) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[zackbloom](https://github.com/zackbloom) |[zlawson-ut](https://github.com/zlawson-ut) |[zachconner](https://github.com/zachconner) |[yafkari](https://github.com/yafkari) |
 
-[<img alt="sercraig" src="https://avatars.githubusercontent.com/u/24261518?v=4&s=117" width="117">](https://github.com/sercraig) |[<img alt="ardeois" src="https://avatars.githubusercontent.com/u/1867939?v=4&s=117" width="117">](https://github.com/ardeois) |[<img alt="CommanderRoot" src="https://avatars.githubusercontent.com/u/4395417?v=4&s=117" width="117">](https://github.com/CommanderRoot) |[<img alt="czj" src="https://avatars.githubusercontent.com/u/14306?v=4&s=117" width="117">](https://github.com/czj) |[<img alt="cbush06" src="https://avatars.githubusercontent.com/u/15720146?v=4&s=117" width="117">](https://github.com/cbush06) |[<img alt="Aarbel" src="https://avatars.githubusercontent.com/u/25119847?v=4&s=117" width="117">](https://github.com/Aarbel) |
+[<img alt="Cruaier" src="https://avatars.githubusercontent.com/u/5204940?v=4&s=117" width="117">](https://github.com/Cruaier) |[<img alt="sercraig" src="https://avatars.githubusercontent.com/u/24261518?v=4&s=117" width="117">](https://github.com/sercraig) |[<img alt="ardeois" src="https://avatars.githubusercontent.com/u/1867939?v=4&s=117" width="117">](https://github.com/ardeois) |[<img alt="CommanderRoot" src="https://avatars.githubusercontent.com/u/4395417?v=4&s=117" width="117">](https://github.com/CommanderRoot) |[<img alt="czj" src="https://avatars.githubusercontent.com/u/14306?v=4&s=117" width="117">](https://github.com/czj) |[<img alt="cbush06" src="https://avatars.githubusercontent.com/u/15720146?v=4&s=117" width="117">](https://github.com/cbush06) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[sercraig](https://github.com/sercraig) |[ardeois](https://github.com/ardeois) |[CommanderRoot](https://github.com/CommanderRoot) |[czj](https://github.com/czj) |[cbush06](https://github.com/cbush06) |[Aarbel](https://github.com/Aarbel) |
+[Cruaier](https://github.com/Cruaier) |[sercraig](https://github.com/sercraig) |[ardeois](https://github.com/ardeois) |[CommanderRoot](https://github.com/CommanderRoot) |[czj](https://github.com/czj) |[cbush06](https://github.com/cbush06) |
 
-[<img alt="cfra" src="https://avatars.githubusercontent.com/u/1347051?v=4&s=117" width="117">](https://github.com/cfra) |[<img alt="csprance" src="https://avatars.githubusercontent.com/u/7902617?v=4&s=117" width="117">](https://github.com/csprance) |[<img alt="prattcmp" src="https://avatars.githubusercontent.com/u/1497950?v=4&s=117" width="117">](https://github.com/prattcmp) |[<img alt="subvertallchris" src="https://avatars.githubusercontent.com/u/4097271?v=4&s=117" width="117">](https://github.com/subvertallchris) |[<img alt="charlybillaud" src="https://avatars.githubusercontent.com/u/31970410?v=4&s=117" width="117">](https://github.com/charlybillaud) |[<img alt="Cretezy" src="https://avatars.githubusercontent.com/u/2672503?v=4&s=117" width="117">](https://github.com/Cretezy) |
+[<img alt="Aarbel" src="https://avatars.githubusercontent.com/u/25119847?v=4&s=117" width="117">](https://github.com/Aarbel) |[<img alt="cfra" src="https://avatars.githubusercontent.com/u/1347051?v=4&s=117" width="117">](https://github.com/cfra) |[<img alt="csprance" src="https://avatars.githubusercontent.com/u/7902617?v=4&s=117" width="117">](https://github.com/csprance) |[<img alt="prattcmp" src="https://avatars.githubusercontent.com/u/1497950?v=4&s=117" width="117">](https://github.com/prattcmp) |[<img alt="subvertallchris" src="https://avatars.githubusercontent.com/u/4097271?v=4&s=117" width="117">](https://github.com/subvertallchris) |[<img alt="charlybillaud" src="https://avatars.githubusercontent.com/u/31970410?v=4&s=117" width="117">](https://github.com/charlybillaud) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[cfra](https://github.com/cfra) |[csprance](https://github.com/csprance) |[prattcmp](https://github.com/prattcmp) |[subvertallchris](https://github.com/subvertallchris) |[charlybillaud](https://github.com/charlybillaud) |[Cretezy](https://github.com/Cretezy) |
+[Aarbel](https://github.com/Aarbel) |[cfra](https://github.com/cfra) |[csprance](https://github.com/csprance) |[prattcmp](https://github.com/prattcmp) |[subvertallchris](https://github.com/subvertallchris) |[charlybillaud](https://github.com/charlybillaud) |
 
-[<img alt="chao" src="https://avatars.githubusercontent.com/u/55872?v=4&s=117" width="117">](https://github.com/chao) |[<img alt="cellvinchung" src="https://avatars.githubusercontent.com/u/5347394?v=4&s=117" width="117">](https://github.com/cellvinchung) |[<img alt="cartfisk" src="https://avatars.githubusercontent.com/u/8764375?v=4&s=117" width="117">](https://github.com/cartfisk) |[<img alt="cyu" src="https://avatars.githubusercontent.com/u/2431?v=4&s=117" width="117">](https://github.com/cyu) |[<img alt="radarhere" src="https://avatars.githubusercontent.com/u/3112309?v=4&s=117" width="117">](https://github.com/radarhere) |[<img alt="kergekacsa" src="https://avatars.githubusercontent.com/u/16637320?v=4&s=117" width="117">](https://github.com/kergekacsa) |
+[<img alt="Cretezy" src="https://avatars.githubusercontent.com/u/2672503?v=4&s=117" width="117">](https://github.com/Cretezy) |[<img alt="chao" src="https://avatars.githubusercontent.com/u/55872?v=4&s=117" width="117">](https://github.com/chao) |[<img alt="cellvinchung" src="https://avatars.githubusercontent.com/u/5347394?v=4&s=117" width="117">](https://github.com/cellvinchung) |[<img alt="cartfisk" src="https://avatars.githubusercontent.com/u/8764375?v=4&s=117" width="117">](https://github.com/cartfisk) |[<img alt="cyu" src="https://avatars.githubusercontent.com/u/2431?v=4&s=117" width="117">](https://github.com/cyu) |[<img alt="radarhere" src="https://avatars.githubusercontent.com/u/3112309?v=4&s=117" width="117">](https://github.com/radarhere) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[chao](https://github.com/chao) |[cellvinchung](https://github.com/cellvinchung) |[cartfisk](https://github.com/cartfisk) |[cyu](https://github.com/cyu) |[radarhere](https://github.com/radarhere) |[kergekacsa](https://github.com/kergekacsa) |
+[Cretezy](https://github.com/Cretezy) |[chao](https://github.com/chao) |[cellvinchung](https://github.com/cellvinchung) |[cartfisk](https://github.com/cartfisk) |[cyu](https://github.com/cyu) |[radarhere](https://github.com/radarhere) |
 
-[<img alt="eliOcs" src="https://avatars.githubusercontent.com/u/1283954?v=4&s=117" width="117">](https://github.com/eliOcs) |[<img alt="yoldar" src="https://avatars.githubusercontent.com/u/1597578?v=4&s=117" width="117">](https://github.com/yoldar) |[<img alt="efbautista" src="https://avatars.githubusercontent.com/u/35430671?v=4&s=117" width="117">](https://github.com/efbautista) |[<img alt="emuell" src="https://avatars.githubusercontent.com/u/11521600?v=4&s=117" width="117">](https://github.com/emuell) |[<img alt="EdgarSantiago93" src="https://avatars.githubusercontent.com/u/14806877?v=4&s=117" width="117">](https://github.com/EdgarSantiago93) |[<img alt="sweetro" src="https://avatars.githubusercontent.com/u/6228717?v=4&s=117" width="117">](https://github.com/sweetro) |
+[<img alt="kergekacsa" src="https://avatars.githubusercontent.com/u/16637320?v=4&s=117" width="117">](https://github.com/kergekacsa) |[<img alt="eliOcs" src="https://avatars.githubusercontent.com/u/1283954?v=4&s=117" width="117">](https://github.com/eliOcs) |[<img alt="yoldar" src="https://avatars.githubusercontent.com/u/1597578?v=4&s=117" width="117">](https://github.com/yoldar) |[<img alt="efbautista" src="https://avatars.githubusercontent.com/u/35430671?v=4&s=117" width="117">](https://github.com/efbautista) |[<img alt="emuell" src="https://avatars.githubusercontent.com/u/11521600?v=4&s=117" width="117">](https://github.com/emuell) |[<img alt="EdgarSantiago93" src="https://avatars.githubusercontent.com/u/14806877?v=4&s=117" width="117">](https://github.com/EdgarSantiago93) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[eliOcs](https://github.com/eliOcs) |[yoldar](https://github.com/yoldar) |[efbautista](https://github.com/efbautista) |[emuell](https://github.com/emuell) |[EdgarSantiago93](https://github.com/EdgarSantiago93) |[sweetro](https://github.com/sweetro) |
+[kergekacsa](https://github.com/kergekacsa) |[eliOcs](https://github.com/eliOcs) |[yoldar](https://github.com/yoldar) |[efbautista](https://github.com/efbautista) |[emuell](https://github.com/emuell) |[EdgarSantiago93](https://github.com/EdgarSantiago93) |
 
-[<img alt="jeetiss" src="https://avatars.githubusercontent.com/u/6726016?v=4&s=117" width="117">](https://github.com/jeetiss) |[<img alt="DennisKofflard" src="https://avatars.githubusercontent.com/u/8669129?v=4&s=117" width="117">](https://github.com/DennisKofflard) |[<img alt="hoangsvit" src="https://avatars.githubusercontent.com/u/11882322?v=4&s=117" width="117">](https://github.com/hoangsvit) |[<img alt="davilima6" src="https://avatars.githubusercontent.com/u/422130?v=4&s=117" width="117">](https://github.com/davilima6) |[<img alt="akizor" src="https://avatars.githubusercontent.com/u/1052439?v=4&s=117" width="117">](https://github.com/akizor) |[<img alt="KaminskiDaniell" src="https://avatars.githubusercontent.com/u/27357868?v=4&s=117" width="117">](https://github.com/KaminskiDaniell) |
+[<img alt="sweetro" src="https://avatars.githubusercontent.com/u/6228717?v=4&s=117" width="117">](https://github.com/sweetro) |[<img alt="jeetiss" src="https://avatars.githubusercontent.com/u/6726016?v=4&s=117" width="117">](https://github.com/jeetiss) |[<img alt="DennisKofflard" src="https://avatars.githubusercontent.com/u/8669129?v=4&s=117" width="117">](https://github.com/DennisKofflard) |[<img alt="hoangsvit" src="https://avatars.githubusercontent.com/u/11882322?v=4&s=117" width="117">](https://github.com/hoangsvit) |[<img alt="davilima6" src="https://avatars.githubusercontent.com/u/422130?v=4&s=117" width="117">](https://github.com/davilima6) |[<img alt="akizor" src="https://avatars.githubusercontent.com/u/1052439?v=4&s=117" width="117">](https://github.com/akizor) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jeetiss](https://github.com/jeetiss) |[DennisKofflard](https://github.com/DennisKofflard) |[hoangsvit](https://github.com/hoangsvit) |[davilima6](https://github.com/davilima6) |[akizor](https://github.com/akizor) |[KaminskiDaniell](https://github.com/KaminskiDaniell) |
+[sweetro](https://github.com/sweetro) |[jeetiss](https://github.com/jeetiss) |[DennisKofflard](https://github.com/DennisKofflard) |[hoangsvit](https://github.com/hoangsvit) |[davilima6](https://github.com/davilima6) |[akizor](https://github.com/akizor) |
 
-[<img alt="Cantabar" src="https://avatars.githubusercontent.com/u/6812207?v=4&s=117" width="117">](https://github.com/Cantabar) |[<img alt="mrboomer" src="https://avatars.githubusercontent.com/u/5942912?v=4&s=117" width="117">](https://github.com/mrboomer) |[<img alt="danilat" src="https://avatars.githubusercontent.com/u/22763?v=4&s=117" width="117">](https://github.com/danilat) |[<img alt="danschalow" src="https://avatars.githubusercontent.com/u/3527437?v=4&s=117" width="117">](https://github.com/danschalow) |[<img alt="danmichaelo" src="https://avatars.githubusercontent.com/u/434495?v=4&s=117" width="117">](https://github.com/danmichaelo) |[<img alt="functino" src="https://avatars.githubusercontent.com/u/415498?v=4&s=117" width="117">](https://github.com/functino) |
+[<img alt="KaminskiDaniell" src="https://avatars.githubusercontent.com/u/27357868?v=4&s=117" width="117">](https://github.com/KaminskiDaniell) |[<img alt="Cantabar" src="https://avatars.githubusercontent.com/u/6812207?v=4&s=117" width="117">](https://github.com/Cantabar) |[<img alt="mrboomer" src="https://avatars.githubusercontent.com/u/5942912?v=4&s=117" width="117">](https://github.com/mrboomer) |[<img alt="danilat" src="https://avatars.githubusercontent.com/u/22763?v=4&s=117" width="117">](https://github.com/danilat) |[<img alt="danschalow" src="https://avatars.githubusercontent.com/u/3527437?v=4&s=117" width="117">](https://github.com/danschalow) |[<img alt="danmichaelo" src="https://avatars.githubusercontent.com/u/434495?v=4&s=117" width="117">](https://github.com/danmichaelo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[Cantabar](https://github.com/Cantabar) |[mrboomer](https://github.com/mrboomer) |[danilat](https://github.com/danilat) |[danschalow](https://github.com/danschalow) |[danmichaelo](https://github.com/danmichaelo) |[functino](https://github.com/functino) |
+[KaminskiDaniell](https://github.com/KaminskiDaniell) |[Cantabar](https://github.com/Cantabar) |[mrboomer](https://github.com/mrboomer) |[danilat](https://github.com/danilat) |[danschalow](https://github.com/danschalow) |[danmichaelo](https://github.com/danmichaelo) |
 
-[<img alt="amitport" src="https://avatars.githubusercontent.com/u/1131991?v=4&s=117" width="117">](https://github.com/amitport) |[<img alt="tekacs" src="https://avatars.githubusercontent.com/u/63247?v=4&s=117" width="117">](https://github.com/tekacs) |[<img alt="Dogfalo" src="https://avatars.githubusercontent.com/u/2775751?v=4&s=117" width="117">](https://github.com/Dogfalo) |[<img alt="aalepis" src="https://avatars.githubusercontent.com/u/35684834?v=4&s=117" width="117">](https://github.com/aalepis) |[<img alt="alexnj" src="https://avatars.githubusercontent.com/u/683500?v=4&s=117" width="117">](https://github.com/alexnj) |[<img alt="asmt3" src="https://avatars.githubusercontent.com/u/1777709?v=4&s=117" width="117">](https://github.com/asmt3) |
+[<img alt="functino" src="https://avatars.githubusercontent.com/u/415498?v=4&s=117" width="117">](https://github.com/functino) |[<img alt="amitport" src="https://avatars.githubusercontent.com/u/1131991?v=4&s=117" width="117">](https://github.com/amitport) |[<img alt="tekacs" src="https://avatars.githubusercontent.com/u/63247?v=4&s=117" width="117">](https://github.com/tekacs) |[<img alt="Dogfalo" src="https://avatars.githubusercontent.com/u/2775751?v=4&s=117" width="117">](https://github.com/Dogfalo) |[<img alt="aalepis" src="https://avatars.githubusercontent.com/u/35684834?v=4&s=117" width="117">](https://github.com/aalepis) |[<img alt="alexnj" src="https://avatars.githubusercontent.com/u/683500?v=4&s=117" width="117">](https://github.com/alexnj) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[amitport](https://github.com/amitport) |[tekacs](https://github.com/tekacs) |[Dogfalo](https://github.com/Dogfalo) |[aalepis](https://github.com/aalepis) |[alexnj](https://github.com/alexnj) |[asmt3](https://github.com/asmt3) |
+[functino](https://github.com/functino) |[amitport](https://github.com/amitport) |[tekacs](https://github.com/tekacs) |[Dogfalo](https://github.com/Dogfalo) |[aalepis](https://github.com/aalepis) |[alexnj](https://github.com/alexnj) |
 
-[<img alt="ahmadissa" src="https://avatars.githubusercontent.com/u/9936573?v=4&s=117" width="117">](https://github.com/ahmadissa) |[<img alt="adritasharma" src="https://avatars.githubusercontent.com/u/29271635?v=4&s=117" width="117">](https://github.com/adritasharma) |[<img alt="Adrrei" src="https://avatars.githubusercontent.com/u/22191685?v=4&s=117" width="117">](https://github.com/Adrrei) |[<img alt="adityapatadia" src="https://avatars.githubusercontent.com/u/1086617?v=4&s=117" width="117">](https://github.com/adityapatadia) |[<img alt="adamvigneault" src="https://avatars.githubusercontent.com/u/18236120?v=4&s=117" width="117">](https://github.com/adamvigneault) |[<img alt="ajh-sr" src="https://avatars.githubusercontent.com/u/71472057?v=4&s=117" width="117">](https://github.com/ajh-sr) |
+[<img alt="asmt3" src="https://avatars.githubusercontent.com/u/1777709?v=4&s=117" width="117">](https://github.com/asmt3) |[<img alt="ahmadissa" src="https://avatars.githubusercontent.com/u/9936573?v=4&s=117" width="117">](https://github.com/ahmadissa) |[<img alt="adritasharma" src="https://avatars.githubusercontent.com/u/29271635?v=4&s=117" width="117">](https://github.com/adritasharma) |[<img alt="Adrrei" src="https://avatars.githubusercontent.com/u/22191685?v=4&s=117" width="117">](https://github.com/Adrrei) |[<img alt="adityapatadia" src="https://avatars.githubusercontent.com/u/1086617?v=4&s=117" width="117">](https://github.com/adityapatadia) |[<img alt="adamvigneault" src="https://avatars.githubusercontent.com/u/18236120?v=4&s=117" width="117">](https://github.com/adamvigneault) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ahmadissa](https://github.com/ahmadissa) |[adritasharma](https://github.com/adritasharma) |[Adrrei](https://github.com/Adrrei) |[adityapatadia](https://github.com/adityapatadia) |[adamvigneault](https://github.com/adamvigneault) |[ajh-sr](https://github.com/ajh-sr) |
+[asmt3](https://github.com/asmt3) |[ahmadissa](https://github.com/ahmadissa) |[adritasharma](https://github.com/adritasharma) |[Adrrei](https://github.com/Adrrei) |[adityapatadia](https://github.com/adityapatadia) |[adamvigneault](https://github.com/adamvigneault) |
 
-[<img alt="adamdottv" src="https://avatars.githubusercontent.com/u/2363879?v=4&s=117" width="117">](https://github.com/adamdottv) |[<img alt="abannach" src="https://avatars.githubusercontent.com/u/43150303?v=4&s=117" width="117">](https://github.com/abannach) |[<img alt="superhawk610" src="https://avatars.githubusercontent.com/u/18172185?v=4&s=117" width="117">](https://github.com/superhawk610) |[<img alt="ajschmidt8" src="https://avatars.githubusercontent.com/u/7400326?v=4&s=117" width="117">](https://github.com/ajschmidt8) |[<img alt="bryanjswift" src="https://avatars.githubusercontent.com/u/9911?v=4&s=117" width="117">](https://github.com/bryanjswift) |[<img alt="bedgerotto" src="https://avatars.githubusercontent.com/u/4459657?v=4&s=117" width="117">](https://github.com/bedgerotto) |
+[<img alt="ajh-sr" src="https://avatars.githubusercontent.com/u/71472057?v=4&s=117" width="117">](https://github.com/ajh-sr) |[<img alt="adamdottv" src="https://avatars.githubusercontent.com/u/2363879?v=4&s=117" width="117">](https://github.com/adamdottv) |[<img alt="abannach" src="https://avatars.githubusercontent.com/u/43150303?v=4&s=117" width="117">](https://github.com/abannach) |[<img alt="superhawk610" src="https://avatars.githubusercontent.com/u/18172185?v=4&s=117" width="117">](https://github.com/superhawk610) |[<img alt="ajschmidt8" src="https://avatars.githubusercontent.com/u/7400326?v=4&s=117" width="117">](https://github.com/ajschmidt8) |[<img alt="bryanjswift" src="https://avatars.githubusercontent.com/u/9911?v=4&s=117" width="117">](https://github.com/bryanjswift) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[adamdottv](https://github.com/adamdottv) |[abannach](https://github.com/abannach) |[superhawk610](https://github.com/superhawk610) |[ajschmidt8](https://github.com/ajschmidt8) |[bryanjswift](https://github.com/bryanjswift) |[bedgerotto](https://github.com/bedgerotto) |
+[ajh-sr](https://github.com/ajh-sr) |[adamdottv](https://github.com/adamdottv) |[abannach](https://github.com/abannach) |[superhawk610](https://github.com/superhawk610) |[ajschmidt8](https://github.com/ajschmidt8) |[bryanjswift](https://github.com/bryanjswift) |
 
-[<img alt="wbaaron" src="https://avatars.githubusercontent.com/u/1048988?v=4&s=117" width="117">](https://github.com/wbaaron) |[<img alt="Quorafind" src="https://avatars.githubusercontent.com/u/13215013?v=4&s=117" width="117">](https://github.com/Quorafind) |[<img alt="bducharme" src="https://avatars.githubusercontent.com/u/4173569?v=4&s=117" width="117">](https://github.com/bducharme) |[<img alt="azizk" src="https://avatars.githubusercontent.com/u/37282?v=4&s=117" width="117">](https://github.com/azizk) |[<img alt="azeemba" src="https://avatars.githubusercontent.com/u/2160795?v=4&s=117" width="117">](https://github.com/azeemba) |[<img alt="ayhankesicioglu" src="https://avatars.githubusercontent.com/u/36304312?v=4&s=117" width="117">](https://github.com/ayhankesicioglu) |
+[<img alt="bedgerotto" src="https://avatars.githubusercontent.com/u/4459657?v=4&s=117" width="117">](https://github.com/bedgerotto) |[<img alt="wbaaron" src="https://avatars.githubusercontent.com/u/1048988?v=4&s=117" width="117">](https://github.com/wbaaron) |[<img alt="Quorafind" src="https://avatars.githubusercontent.com/u/13215013?v=4&s=117" width="117">](https://github.com/Quorafind) |[<img alt="bducharme" src="https://avatars.githubusercontent.com/u/4173569?v=4&s=117" width="117">](https://github.com/bducharme) |[<img alt="azizk" src="https://avatars.githubusercontent.com/u/37282?v=4&s=117" width="117">](https://github.com/azizk) |[<img alt="azeemba" src="https://avatars.githubusercontent.com/u/2160795?v=4&s=117" width="117">](https://github.com/azeemba) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[wbaaron](https://github.com/wbaaron) |[Quorafind](https://github.com/Quorafind) |[bducharme](https://github.com/bducharme) |[azizk](https://github.com/azizk) |[azeemba](https://github.com/azeemba) |[ayhankesicioglu](https://github.com/ayhankesicioglu) |
+[bedgerotto](https://github.com/bedgerotto) |[wbaaron](https://github.com/wbaaron) |[Quorafind](https://github.com/Quorafind) |[bducharme](https://github.com/bducharme) |[azizk](https://github.com/azizk) |[azeemba](https://github.com/azeemba) |
 
-[<img alt="atsawin" src="https://avatars.githubusercontent.com/u/666663?v=4&s=117" width="117">](https://github.com/atsawin) |[<img alt="ash-jc-allen" src="https://avatars.githubusercontent.com/u/39652331?v=4&s=117" width="117">](https://github.com/ash-jc-allen) |[<img alt="apuyou" src="https://avatars.githubusercontent.com/u/520053?v=4&s=117" width="117">](https://github.com/apuyou) |[<img alt="arthurdenner" src="https://avatars.githubusercontent.com/u/13774309?v=4&s=117" width="117">](https://github.com/arthurdenner) |[<img alt="Abourass" src="https://avatars.githubusercontent.com/u/39917231?v=4&s=117" width="117">](https://github.com/Abourass) |[<img alt="tyndria" src="https://avatars.githubusercontent.com/u/17138916?v=4&s=117" width="117">](https://github.com/tyndria) |
+[<img alt="ayhankesicioglu" src="https://avatars.githubusercontent.com/u/36304312?v=4&s=117" width="117">](https://github.com/ayhankesicioglu) |[<img alt="atsawin" src="https://avatars.githubusercontent.com/u/666663?v=4&s=117" width="117">](https://github.com/atsawin) |[<img alt="ash-jc-allen" src="https://avatars.githubusercontent.com/u/39652331?v=4&s=117" width="117">](https://github.com/ash-jc-allen) |[<img alt="apuyou" src="https://avatars.githubusercontent.com/u/520053?v=4&s=117" width="117">](https://github.com/apuyou) |[<img alt="arthurdenner" src="https://avatars.githubusercontent.com/u/13774309?v=4&s=117" width="117">](https://github.com/arthurdenner) |[<img alt="Abourass" src="https://avatars.githubusercontent.com/u/39917231?v=4&s=117" width="117">](https://github.com/Abourass) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[atsawin](https://github.com/atsawin) |[ash-jc-allen](https://github.com/ash-jc-allen) |[apuyou](https://github.com/apuyou) |[arthurdenner](https://github.com/arthurdenner) |[Abourass](https://github.com/Abourass) |[tyndria](https://github.com/tyndria) |
+[ayhankesicioglu](https://github.com/ayhankesicioglu) |[atsawin](https://github.com/atsawin) |[ash-jc-allen](https://github.com/ash-jc-allen) |[apuyou](https://github.com/apuyou) |[arthurdenner](https://github.com/arthurdenner) |[Abourass](https://github.com/Abourass) |
 
-[<img alt="anthony0030" src="https://avatars.githubusercontent.com/u/13033263?v=4&s=117" width="117">](https://github.com/anthony0030) |[<img alt="andychongyz" src="https://avatars.githubusercontent.com/u/12697240?v=4&s=117" width="117">](https://github.com/andychongyz) |[<img alt="andrii-bodnar" src="https://avatars.githubusercontent.com/u/29282228?v=4&s=117" width="117">](https://github.com/andrii-bodnar) |[<img alt="superandrew213" src="https://avatars.githubusercontent.com/u/13059204?v=4&s=117" width="117">](https://github.com/superandrew213) |[<img alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117" width="117">](https://github.com/firesharkstudios) |[<img alt="kaspermeinema" src="https://avatars.githubusercontent.com/u/73821331?v=4&s=117" width="117">](https://github.com/kaspermeinema) |
+[<img alt="tyndria" src="https://avatars.githubusercontent.com/u/17138916?v=4&s=117" width="117">](https://github.com/tyndria) |[<img alt="anthony0030" src="https://avatars.githubusercontent.com/u/13033263?v=4&s=117" width="117">](https://github.com/anthony0030) |[<img alt="andychongyz" src="https://avatars.githubusercontent.com/u/12697240?v=4&s=117" width="117">](https://github.com/andychongyz) |[<img alt="andrii-bodnar" src="https://avatars.githubusercontent.com/u/29282228?v=4&s=117" width="117">](https://github.com/andrii-bodnar) |[<img alt="superandrew213" src="https://avatars.githubusercontent.com/u/13059204?v=4&s=117" width="117">](https://github.com/superandrew213) |[<img alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117" width="117">](https://github.com/firesharkstudios) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[anthony0030](https://github.com/anthony0030) |[andychongyz](https://github.com/andychongyz) |[andrii-bodnar](https://github.com/andrii-bodnar) |[superandrew213](https://github.com/superandrew213) |[firesharkstudios](https://github.com/firesharkstudios) |[kaspermeinema](https://github.com/kaspermeinema) |
+[tyndria](https://github.com/tyndria) |[anthony0030](https://github.com/anthony0030) |[andychongyz](https://github.com/andychongyz) |[andrii-bodnar](https://github.com/andrii-bodnar) |[superandrew213](https://github.com/superandrew213) |[firesharkstudios](https://github.com/firesharkstudios) |
 
 [<img alt="tykarol" src="https://avatars.githubusercontent.com/u/9386320?v=4&s=117" width="117">](https://github.com/tykarol) |[<img alt="jvelten" src="https://avatars.githubusercontent.com/u/48118068?v=4&s=117" width="117">](https://github.com/jvelten) |[<img alt="mellow-fellow" src="https://avatars.githubusercontent.com/u/19280122?v=4&s=117" width="117">](https://github.com/mellow-fellow) |[<img alt="jmontoyaa" src="https://avatars.githubusercontent.com/u/158935?v=4&s=117" width="117">](https://github.com/jmontoyaa) |[<img alt="jcalonso" src="https://avatars.githubusercontent.com/u/664474?v=4&s=117" width="117">](https://github.com/jcalonso) |[<img alt="jbelej" src="https://avatars.githubusercontent.com/u/2229202?v=4&s=117" width="117">](https://github.com/jbelej) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 [tykarol](https://github.com/tykarol) |[jvelten](https://github.com/jvelten) |[mellow-fellow](https://github.com/mellow-fellow) |[jmontoyaa](https://github.com/jmontoyaa) |[jcalonso](https://github.com/jcalonso) |[jbelej](https://github.com/jbelej) |
 
-[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117" width="117">](https://github.com/jorgeepc) |[<img alt="jondewoo" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117" width="117">](https://github.com/jondewoo) |[<img alt="jonathanarbely" src="https://avatars.githubusercontent.com/u/18177203?v=4&s=117" width="117">](https://github.com/jonathanarbely) |[<img alt="jsanchez034" src="https://avatars.githubusercontent.com/u/761087?v=4&s=117" width="117">](https://github.com/jsanchez034) |[<img alt="Jokcy" src="https://avatars.githubusercontent.com/u/2088642?v=4&s=117" width="117">](https://github.com/Jokcy) |
+[<img alt="jbelej" src="https://avatars.githubusercontent.com/u/2229202?v=4&s=117" width="117">](https://github.com/jbelej) |[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117" width="117">](https://github.com/jorgeepc) |[<img alt="jondewoo" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117" width="117">](https://github.com/jondewoo) |[<img alt="jonathanarbely" src="https://avatars.githubusercontent.com/u/18177203?v=4&s=117" width="117">](https://github.com/jonathanarbely) |[<img alt="jsanchez034" src="https://avatars.githubusercontent.com/u/761087?v=4&s=117" width="117">](https://github.com/jsanchez034) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jszobody](https://github.com/jszobody) |[jorgeepc](https://github.com/jorgeepc) |[jondewoo](https://github.com/jondewoo) |[jonathanarbely](https://github.com/jonathanarbely) |[jsanchez034](https://github.com/jsanchez034) |[Jokcy](https://github.com/Jokcy) |
+[jbelej](https://github.com/jbelej) |[jszobody](https://github.com/jszobody) |[jorgeepc](https://github.com/jorgeepc) |[jondewoo](https://github.com/jondewoo) |[jonathanarbely](https://github.com/jonathanarbely) |[jsanchez034](https://github.com/jsanchez034) |
 
-[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |[<img alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117" width="117">](https://github.com/profsmallpine) |[<img alt="IanVS" src="https://avatars.githubusercontent.com/u/4616705?v=4&s=117" width="117">](https://github.com/IanVS) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |
+[<img alt="Jokcy" src="https://avatars.githubusercontent.com/u/2088642?v=4&s=117" width="117">](https://github.com/Jokcy) |[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |[<img alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117" width="117">](https://github.com/profsmallpine) |[<img alt="IanVS" src="https://avatars.githubusercontent.com/u/4616705?v=4&s=117" width="117">](https://github.com/IanVS) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[chromacoma](https://github.com/chromacoma) |[profsmallpine](https://github.com/profsmallpine) |[IanVS](https://github.com/IanVS) |[Lucklj521](https://github.com/Lucklj521) |[lucax88x](https://github.com/lucax88x) |[lucaperret](https://github.com/lucaperret) |
+[Jokcy](https://github.com/Jokcy) |[chromacoma](https://github.com/chromacoma) |[profsmallpine](https://github.com/profsmallpine) |[IanVS](https://github.com/IanVS) |[Lucklj521](https://github.com/Lucklj521) |[lucax88x](https://github.com/lucax88x) |
 
-[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="louim" src="https://avatars.githubusercontent.com/u/923718?v=4&s=117" width="117">](https://github.com/louim) |[<img alt="dolphinigle" src="https://avatars.githubusercontent.com/u/7020472?v=4&s=117" width="117">](https://github.com/dolphinigle) |[<img alt="leomelzer" src="https://avatars.githubusercontent.com/u/23313?v=4&s=117" width="117">](https://github.com/leomelzer) |[<img alt="leods92" src="https://avatars.githubusercontent.com/u/879395?v=4&s=117" width="117">](https://github.com/leods92) |[<img alt="galli-leo" src="https://avatars.githubusercontent.com/u/5339762?v=4&s=117" width="117">](https://github.com/galli-leo) |
+[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="louim" src="https://avatars.githubusercontent.com/u/923718?v=4&s=117" width="117">](https://github.com/louim) |[<img alt="dolphinigle" src="https://avatars.githubusercontent.com/u/7020472?v=4&s=117" width="117">](https://github.com/dolphinigle) |[<img alt="leomelzer" src="https://avatars.githubusercontent.com/u/23313?v=4&s=117" width="117">](https://github.com/leomelzer) |[<img alt="leods92" src="https://avatars.githubusercontent.com/u/879395?v=4&s=117" width="117">](https://github.com/leods92) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ombr](https://github.com/ombr) |[louim](https://github.com/louim) |[dolphinigle](https://github.com/dolphinigle) |[leomelzer](https://github.com/leomelzer) |[leods92](https://github.com/leods92) |[galli-leo](https://github.com/galli-leo) |
+[lucaperret](https://github.com/lucaperret) |[ombr](https://github.com/ombr) |[louim](https://github.com/louim) |[dolphinigle](https://github.com/dolphinigle) |[leomelzer](https://github.com/leomelzer) |[leods92](https://github.com/leods92) |
 
-[<img alt="dviry" src="https://avatars.githubusercontent.com/u/1230260?v=4&s=117" width="117">](https://github.com/dviry) |[<img alt="larowlan" src="https://avatars.githubusercontent.com/u/555254?v=4&s=117" width="117">](https://github.com/larowlan) |[<img alt="leaanthony" src="https://avatars.githubusercontent.com/u/1943904?v=4&s=117" width="117">](https://github.com/leaanthony) |[<img alt="hoangbits" src="https://avatars.githubusercontent.com/u/7990827?v=4&s=117" width="117">](https://github.com/hoangbits) |[<img alt="labohkip81" src="https://avatars.githubusercontent.com/u/36964869?v=4&s=117" width="117">](https://github.com/labohkip81) |[<img alt="kyleparisi" src="https://avatars.githubusercontent.com/u/1286753?v=4&s=117" width="117">](https://github.com/kyleparisi) |
+[<img alt="galli-leo" src="https://avatars.githubusercontent.com/u/5339762?v=4&s=117" width="117">](https://github.com/galli-leo) |[<img alt="dviry" src="https://avatars.githubusercontent.com/u/1230260?v=4&s=117" width="117">](https://github.com/dviry) |[<img alt="larowlan" src="https://avatars.githubusercontent.com/u/555254?v=4&s=117" width="117">](https://github.com/larowlan) |[<img alt="leaanthony" src="https://avatars.githubusercontent.com/u/1943904?v=4&s=117" width="117">](https://github.com/leaanthony) |[<img alt="hoangbits" src="https://avatars.githubusercontent.com/u/7990827?v=4&s=117" width="117">](https://github.com/hoangbits) |[<img alt="labohkip81" src="https://avatars.githubusercontent.com/u/36964869?v=4&s=117" width="117">](https://github.com/labohkip81) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[dviry](https://github.com/dviry) |[larowlan](https://github.com/larowlan) |[leaanthony](https://github.com/leaanthony) |[hoangbits](https://github.com/hoangbits) |[labohkip81](https://github.com/labohkip81) |[kyleparisi](https://github.com/kyleparisi) |
+[galli-leo](https://github.com/galli-leo) |[dviry](https://github.com/dviry) |[larowlan](https://github.com/larowlan) |[leaanthony](https://github.com/leaanthony) |[hoangbits](https://github.com/hoangbits) |[labohkip81](https://github.com/labohkip81) |
 
-[<img alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117" width="117">](https://github.com/elkebab) |[<img alt="kidonng" src="https://avatars.githubusercontent.com/u/44045911?v=4&s=117" width="117">](https://github.com/kidonng) |[<img alt="kevin-west-10x" src="https://avatars.githubusercontent.com/u/65194914?v=4&s=117" width="117">](https://github.com/kevin-west-10x) |[<img alt="huydod" src="https://avatars.githubusercontent.com/u/37580530?v=4&s=117" width="117">](https://github.com/huydod) |[<img alt="HussainAlkhalifah" src="https://avatars.githubusercontent.com/u/43642162?v=4&s=117" width="117">](https://github.com/HussainAlkhalifah) |[<img alt="HughbertD" src="https://avatars.githubusercontent.com/u/1580021?v=4&s=117" width="117">](https://github.com/HughbertD) |
+[<img alt="kyleparisi" src="https://avatars.githubusercontent.com/u/1286753?v=4&s=117" width="117">](https://github.com/kyleparisi) |[<img alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117" width="117">](https://github.com/elkebab) |[<img alt="kidonng" src="https://avatars.githubusercontent.com/u/44045911?v=4&s=117" width="117">](https://github.com/kidonng) |[<img alt="kevin-west-10x" src="https://avatars.githubusercontent.com/u/65194914?v=4&s=117" width="117">](https://github.com/kevin-west-10x) |[<img alt="huydod" src="https://avatars.githubusercontent.com/u/37580530?v=4&s=117" width="117">](https://github.com/huydod) |[<img alt="HussainAlkhalifah" src="https://avatars.githubusercontent.com/u/43642162?v=4&s=117" width="117">](https://github.com/HussainAlkhalifah) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[elkebab](https://github.com/elkebab) |[kidonng](https://github.com/kidonng) |[kevin-west-10x](https://github.com/kevin-west-10x) |[huydod](https://github.com/huydod) |[HussainAlkhalifah](https://github.com/HussainAlkhalifah) |[HughbertD](https://github.com/HughbertD) |
+[kyleparisi](https://github.com/kyleparisi) |[elkebab](https://github.com/elkebab) |[kidonng](https://github.com/kidonng) |[kevin-west-10x](https://github.com/kevin-west-10x) |[huydod](https://github.com/huydod) |[HussainAlkhalifah](https://github.com/HussainAlkhalifah) |
 
-[<img alt="hiromi2424" src="https://avatars.githubusercontent.com/u/191297?v=4&s=117" width="117">](https://github.com/hiromi2424) |[<img alt="giacomocerquone" src="https://avatars.githubusercontent.com/u/9303791?v=4&s=117" width="117">](https://github.com/giacomocerquone) |[<img alt="roenschg" src="https://avatars.githubusercontent.com/u/9590236?v=4&s=117" width="117">](https://github.com/roenschg) |[<img alt="gjungb" src="https://avatars.githubusercontent.com/u/3391068?v=4&s=117" width="117">](https://github.com/gjungb) |[<img alt="geoffappleford" src="https://avatars.githubusercontent.com/u/731678?v=4&s=117" width="117">](https://github.com/geoffappleford) |[<img alt="gabiganam" src="https://avatars.githubusercontent.com/u/28859646?v=4&s=117" width="117">](https://github.com/gabiganam) |
+[<img alt="HughbertD" src="https://avatars.githubusercontent.com/u/1580021?v=4&s=117" width="117">](https://github.com/HughbertD) |[<img alt="hiromi2424" src="https://avatars.githubusercontent.com/u/191297?v=4&s=117" width="117">](https://github.com/hiromi2424) |[<img alt="giacomocerquone" src="https://avatars.githubusercontent.com/u/9303791?v=4&s=117" width="117">](https://github.com/giacomocerquone) |[<img alt="roenschg" src="https://avatars.githubusercontent.com/u/9590236?v=4&s=117" width="117">](https://github.com/roenschg) |[<img alt="gjungb" src="https://avatars.githubusercontent.com/u/3391068?v=4&s=117" width="117">](https://github.com/gjungb) |[<img alt="geoffappleford" src="https://avatars.githubusercontent.com/u/731678?v=4&s=117" width="117">](https://github.com/geoffappleford) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[hiromi2424](https://github.com/hiromi2424) |[giacomocerquone](https://github.com/giacomocerquone) |[roenschg](https://github.com/roenschg) |[gjungb](https://github.com/gjungb) |[geoffappleford](https://github.com/geoffappleford) |[gabiganam](https://github.com/gabiganam) |
+[HughbertD](https://github.com/HughbertD) |[hiromi2424](https://github.com/hiromi2424) |[giacomocerquone](https://github.com/giacomocerquone) |[roenschg](https://github.com/roenschg) |[gjungb](https://github.com/gjungb) |[geoffappleford](https://github.com/geoffappleford) |
 
-[<img alt="fuadscodes" src="https://avatars.githubusercontent.com/u/60370584?v=4&s=117" width="117">](https://github.com/fuadscodes) |[<img alt="dtrucs" src="https://avatars.githubusercontent.com/u/1926041?v=4&s=117" width="117">](https://github.com/dtrucs) |[<img alt="ferdiusa" src="https://avatars.githubusercontent.com/u/1997982?v=4&s=117" width="117">](https://github.com/ferdiusa) |[<img alt="fgallinari" src="https://avatars.githubusercontent.com/u/6473638?v=4&s=117" width="117">](https://github.com/fgallinari) |[<img alt="Gkleinereva" src="https://avatars.githubusercontent.com/u/23621633?v=4&s=117" width="117">](https://github.com/Gkleinereva) |[<img alt="epexa" src="https://avatars.githubusercontent.com/u/2198826?v=4&s=117" width="117">](https://github.com/epexa) |
+[<img alt="gabiganam" src="https://avatars.githubusercontent.com/u/28859646?v=4&s=117" width="117">](https://github.com/gabiganam) |[<img alt="fuadscodes" src="https://avatars.githubusercontent.com/u/60370584?v=4&s=117" width="117">](https://github.com/fuadscodes) |[<img alt="dtrucs" src="https://avatars.githubusercontent.com/u/1926041?v=4&s=117" width="117">](https://github.com/dtrucs) |[<img alt="ferdiusa" src="https://avatars.githubusercontent.com/u/1997982?v=4&s=117" width="117">](https://github.com/ferdiusa) |[<img alt="fgallinari" src="https://avatars.githubusercontent.com/u/6473638?v=4&s=117" width="117">](https://github.com/fgallinari) |[<img alt="Gkleinereva" src="https://avatars.githubusercontent.com/u/23621633?v=4&s=117" width="117">](https://github.com/Gkleinereva) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[fuadscodes](https://github.com/fuadscodes) |[dtrucs](https://github.com/dtrucs) |[ferdiusa](https://github.com/ferdiusa) |[fgallinari](https://github.com/fgallinari) |[Gkleinereva](https://github.com/Gkleinereva) |[epexa](https://github.com/epexa) |
+[gabiganam](https://github.com/gabiganam) |[fuadscodes](https://github.com/fuadscodes) |[dtrucs](https://github.com/dtrucs) |[ferdiusa](https://github.com/ferdiusa) |[fgallinari](https://github.com/fgallinari) |[Gkleinereva](https://github.com/Gkleinereva) |
 
-[<img alt="EnricoSottile" src="https://avatars.githubusercontent.com/u/10349653?v=4&s=117" width="117">](https://github.com/EnricoSottile) |[<img alt="elliotdickison" src="https://avatars.githubusercontent.com/u/2523678?v=4&s=117" width="117">](https://github.com/elliotdickison) |[<img alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117" width="117">](https://github.com/theJoeBiz) |[<img alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117" width="117">](https://github.com/Jmales) |[<img alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117" width="117">](https://github.com/jessica-coursera) |[<img alt="vith" src="https://avatars.githubusercontent.com/u/3265539?v=4&s=117" width="117">](https://github.com/vith) |
+[<img alt="epexa" src="https://avatars.githubusercontent.com/u/2198826?v=4&s=117" width="117">](https://github.com/epexa) |[<img alt="EnricoSottile" src="https://avatars.githubusercontent.com/u/10349653?v=4&s=117" width="117">](https://github.com/EnricoSottile) |[<img alt="elliotdickison" src="https://avatars.githubusercontent.com/u/2523678?v=4&s=117" width="117">](https://github.com/elliotdickison) |[<img alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117" width="117">](https://github.com/theJoeBiz) |[<img alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117" width="117">](https://github.com/Jmales) |[<img alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117" width="117">](https://github.com/jessica-coursera) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[EnricoSottile](https://github.com/EnricoSottile) |[elliotdickison](https://github.com/elliotdickison) |[theJoeBiz](https://github.com/theJoeBiz) |[Jmales](https://github.com/Jmales) |[jessica-coursera](https://github.com/jessica-coursera) |[vith](https://github.com/vith) |
+[epexa](https://github.com/epexa) |[EnricoSottile](https://github.com/EnricoSottile) |[elliotdickison](https://github.com/elliotdickison) |[theJoeBiz](https://github.com/theJoeBiz) |[Jmales](https://github.com/Jmales) |[jessica-coursera](https://github.com/jessica-coursera) |
 
-[<img alt="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117" width="117">](https://github.com/janwilts) |[<img alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117" width="117">](https://github.com/janklimo) |[<img alt="jamestiotio" src="https://avatars.githubusercontent.com/u/18364745?v=4&s=117" width="117">](https://github.com/jamestiotio) |[<img alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117" width="117">](https://github.com/jcjmcclean) |[<img alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117" width="117">](https://github.com/Jbithell) |[<img alt="JakubHaladej" src="https://avatars.githubusercontent.com/u/77832677?v=4&s=117" width="117">](https://github.com/JakubHaladej) |
+[<img alt="vith" src="https://avatars.githubusercontent.com/u/3265539?v=4&s=117" width="117">](https://github.com/vith) |[<img alt="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117" width="117">](https://github.com/janwilts) |[<img alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117" width="117">](https://github.com/janklimo) |[<img alt="jamestiotio" src="https://avatars.githubusercontent.com/u/18364745?v=4&s=117" width="117">](https://github.com/jamestiotio) |[<img alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117" width="117">](https://github.com/jcjmcclean) |[<img alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117" width="117">](https://github.com/Jbithell) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[janwilts](https://github.com/janwilts) |[janklimo](https://github.com/janklimo) |[jamestiotio](https://github.com/jamestiotio) |[jcjmcclean](https://github.com/jcjmcclean) |[Jbithell](https://github.com/Jbithell) |[JakubHaladej](https://github.com/JakubHaladej) |
+[vith](https://github.com/vith) |[janwilts](https://github.com/janwilts) |[janklimo](https://github.com/janklimo) |[jamestiotio](https://github.com/jamestiotio) |[jcjmcclean](https://github.com/jcjmcclean) |[Jbithell](https://github.com/Jbithell) |
 
-[<img alt="jakemcallister" src="https://avatars.githubusercontent.com/u/1185699?v=4&s=117" width="117">](https://github.com/jakemcallister) |[<img alt="gaejabong" src="https://avatars.githubusercontent.com/u/978944?v=4&s=117" width="117">](https://github.com/gaejabong) |[<img alt="JacobMGEvans" src="https://avatars.githubusercontent.com/u/27247160?v=4&s=117" width="117">](https://github.com/JacobMGEvans) |[<img alt="mazoruss" src="https://avatars.githubusercontent.com/u/17625190?v=4&s=117" width="117">](https://github.com/mazoruss) |[<img alt="GreenJimmy" src="https://avatars.githubusercontent.com/u/39386?v=4&s=117" width="117">](https://github.com/GreenJimmy) |[<img alt="intenzive" src="https://avatars.githubusercontent.com/u/11055931?v=4&s=117" width="117">](https://github.com/intenzive) |
+[<img alt="JakubHaladej" src="https://avatars.githubusercontent.com/u/77832677?v=4&s=117" width="117">](https://github.com/JakubHaladej) |[<img alt="jakemcallister" src="https://avatars.githubusercontent.com/u/1185699?v=4&s=117" width="117">](https://github.com/jakemcallister) |[<img alt="gaejabong" src="https://avatars.githubusercontent.com/u/978944?v=4&s=117" width="117">](https://github.com/gaejabong) |[<img alt="JacobMGEvans" src="https://avatars.githubusercontent.com/u/27247160?v=4&s=117" width="117">](https://github.com/JacobMGEvans) |[<img alt="mazoruss" src="https://avatars.githubusercontent.com/u/17625190?v=4&s=117" width="117">](https://github.com/mazoruss) |[<img alt="GreenJimmy" src="https://avatars.githubusercontent.com/u/39386?v=4&s=117" width="117">](https://github.com/GreenJimmy) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jakemcallister](https://github.com/jakemcallister) |[gaejabong](https://github.com/gaejabong) |[JacobMGEvans](https://github.com/JacobMGEvans) |[mazoruss](https://github.com/mazoruss) |[GreenJimmy](https://github.com/GreenJimmy) |[intenzive](https://github.com/intenzive) |
+[JakubHaladej](https://github.com/JakubHaladej) |[jakemcallister](https://github.com/jakemcallister) |[gaejabong](https://github.com/gaejabong) |[JacobMGEvans](https://github.com/JacobMGEvans) |[mazoruss](https://github.com/mazoruss) |[GreenJimmy](https://github.com/GreenJimmy) |
 
-[<img alt="NaxYo" src="https://avatars.githubusercontent.com/u/1963876?v=4&s=117" width="117">](https://github.com/NaxYo) |[<img alt="ishendyweb" src="https://avatars.githubusercontent.com/u/10582418?v=4&s=117" width="117">](https://github.com/ishendyweb) |
-:---: |:---: |
-[NaxYo](https://github.com/NaxYo) |[ishendyweb](https://github.com/ishendyweb) |
+[<img alt="intenzive" src="https://avatars.githubusercontent.com/u/11055931?v=4&s=117" width="117">](https://github.com/intenzive) |[<img alt="NaxYo" src="https://avatars.githubusercontent.com/u/1963876?v=4&s=117" width="117">](https://github.com/NaxYo) |[<img alt="ishendyweb" src="https://avatars.githubusercontent.com/u/10582418?v=4&s=117" width="117">](https://github.com/ishendyweb) |
+:---: |:---: |:---: |
+[intenzive](https://github.com/intenzive) |[NaxYo](https://github.com/NaxYo) |[ishendyweb](https://github.com/ishendyweb) |
 
 <!--/contributors-->
 
@@ -472,4 +472,3 @@ We use Browserstack for manual testing <a href="https://www.browserstack.com" ta
 ## License
 
 [The MIT License](LICENSE).
-E).

+ 3 - 0
docs/README.md

@@ -0,0 +1,3 @@
+# Uppy documentation
+
+To build the documentation, see <https://github.com/transloadit/uppy.io>.

+ 935 - 0
docs/companion.md

@@ -0,0 +1,935 @@
+---
+sidebar_position: 4
+---
+
+# Companion
+
+Companion is an open source server application which **takes away the complexity
+of authentication and the cost of downloading files from remote sources**, such
+as Instagram, Google Drive, and others. Companion is a server-to-server
+orchestrator that streams files from a source to a destination, and files are
+never stored in Companion. Companion can run either as a standalone
+(self-hosted) application, [Transloadit-hosted](#hosted), or plugged in as an
+Express middleware into an existing application. The Uppy client requests remote
+files from Companion, which it will download and simultaneously upload to your
+[Tus server](/docs/tus), [AWS bucket](/docs/aws-s3), or any server that supports
+[PUT, POST or Multipart uploads](/docs/xhr-upload).
+
+This means a user uploading a 5GB video from Google Drive from their phone isn’t
+eating into their data plans and you don’t have to worry about implementing
+OAuth.
+
+## When should I use it?
+
+If you want to let users download files from [Box][], [Dropbox][], [Facebook][],
+[Google Drive][googledrive], [Instagram][], [OneDrive][], [Unsplash][], [Import
+from URL][url], or [Zoom][] — you need Companion.
+
+Companion supports the same [uploaders](/docs/guides/choosing-uploader) as Uppy:
+[Tus](/docs/tus), [AWS S3](/docs/aws-s3), and [regular multipart](/docs/tus).
+But instead of manually setting a plugin, Uppy sends along a header with the
+uploader and Companion will use the same on the server. This means if you are
+using [Tus](/docs/tus) for your local uploads, you can send your remote uploads
+to the same Tus server (and likewise for your AWS S3 bucket).
+
+:::note
+
+Companion only deals with _remote_ files, _local_ files are still uploaded from
+the client with your upload plugin.
+
+:::
+
+## Hosted
+
+Using [Transloadit][] services comes with a hosted version of Companion so you
+don’t have to worry about hosting your own server. Whether you are on a free or
+paid Transloadit [plan](https://transloadit.com/pricing/), you can use
+Companion. It’s not possible to rent a Companion server without a Transloadit
+plan.
+
+[**Sign-up for a (free) plan**](https://transloadit.com/pricing/).
+
+:::tip
+
+Choosing Transloadit for your file services also comes with credentials for all
+remote providers. This means you don’t have to waste time going through the
+approval process of every app. You can still add your own credentials in the
+Transloadit admin page if you want.
+
+:::
+
+:::info
+
+Downloading and uploading files through Companion doesn’t count towards your
+[monthly quota](https://transloadit.com/docs/faq/1gb-worth/), it’s a way for
+files to arrive at Transloadit servers, much like Uppy.
+
+:::
+
+## Installation & use
+
+Companion is installed from npm. Depending on how you want to run Companion, the
+install process is slightly different. Companion can be integrated as middleware
+into your [Express](https://expressjs.com/) app or as a standalone server. Most
+people probably want to run it as a standalone server, while the middleware
+could be used to further customise Companion or integrate it into your own HTTP
+server code.
+
+:::note
+
+Since v2, you need to be running `node.js >= v10.20.1` to use Companion. More
+information in the
+[migrating to 2.0](/docs/guides/migration-guides/#migrate-from-uppy-1x-to-2x)
+guide.
+
+Windows is not a supported platform right now. It may work, and we’re happy to
+accept improvements in this area, but we can’t provide support.
+
+:::
+
+### Standalone mode
+
+You can use the standalone version if you want to run Companion as it’s own
+Node.js process. It’s a configured Express server with sessions, logging, and
+security best practices. First you’ll typically want to install it globally:
+
+```bash
+npm install -g @uppy/companion
+```
+
+Standalone Companion will always serve HTTP (not HTTPS) and expects a reverse
+proxy with SSL termination in front of it when running in production. See
+[`COMPANION_PROTOCOL`](#server) for more information.
+
+Companion ships with an executable file (`bin/companion`) which is the
+standalone server. Unlike the middleware version, options are set via
+environment variables.
+
+:::info
+
+Checkout [options](#options) for the available options in JS and environment
+variable formats.
+
+:::
+
+You need at least these three to get started:
+
+```bash
+export COMPANION_SECRET="shh!Issa Secret!"
+export COMPANION_DOMAIN="YOUR SERVER DOMAIN"
+export COMPANION_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY"
+```
+
+Then run:
+
+```bash
+companion
+```
+
+You can also pass in the path to your JSON config file, like so:
+
+```bash
+companion --config /path/to/companion.json
+```
+
+You may also want to run Companion in a process manager like
+[PM2](https://pm2.keymetrics.io/) to make sure it gets restarted on upon
+crashing as well as allowing scaling to many instances.
+
+### Express middleware mode
+
+First install it into your Node.js project with your favorite package manager:
+
+```bash
+npm install @uppy/companion
+```
+
+To plug Companion into an existing server, call its `.app` method, passing in an
+[options](#options) object as a parameter. This returns a server instance that
+you can mount on a route in your Express app.
+
+```js
+import express from 'express';
+import bodyParser from 'body-parser';
+import session from 'express-session';
+import companion from '@uppy/companion';
+
+const app = express();
+
+// Companion requires body-parser and express-session middleware.
+// You can add it like this if you use those throughout your app.
+//
+// If you are using something else in your app, you can add these
+// middlewares in the same subpath as Companion instead.
+app.use(bodyParser.json());
+app.use(session({ secret: 'some secrety secret' }));
+
+const companionOptions = {
+	providerOptions: {
+		drive: {
+			key: 'GOOGLE_DRIVE_KEY',
+			secret: 'GOOGLE_DRIVE_SECRET',
+		},
+	},
+	server: {
+		host: 'localhost:3020',
+		protocol: 'http',
+		// Default installations normally don't need a path.
+		// However if you specify a `path`, you MUST specify
+		// the same path in `app.use()` below,
+		// e.g. app.use('/companion', companionApp)
+		// path: '/companion',
+	},
+	filePath: '/path/to/folder/',
+};
+
+const { app: companionApp } = companion.app(companionOptions);
+app.use(companionApp);
+```
+
+Companion uses WebSockets to communicate progress, errors, and successes to the
+client. This is what Uppy listens to to update it’s internal state and UI.
+
+Add the Companion WebSocket server using the `companion.socket` function:
+
+```js
+const server = app.listen(PORT);
+
+companion.socket(server);
+```
+
+If WebSockets fail for some reason Uppy and Companion will fallback to HTTP
+polling.
+
+### Running many instances
+
+We recommend running at least two instances in production, so that if the
+Node.js event loop gets blocked by one or more requests (due to a bug or spike
+in traffic), it doesn’t also block or slow down all other requests as well (as
+Node.js is single threaded).
+
+As an example for scale, one enterprise customer of Transloadit, who self-hosts
+Companion to power an education service that is used by many universities
+globally, deploys 7 Companion instances. Their earlier solution ran on 35
+instances. In our general experience Companion will saturate network interface
+cards before other resources on commodity virtual servers (`c5d.2xlarge` for
+instance).
+
+Your mileage may vary, so we recommend to add observability. You can let
+Prometheus crawl the `/metrics` endpoint and graph that with Grafana for
+instance.
+
+#### Using unique endpoints
+
+One option is to run many instances with each instance having its own unique
+endpoint. This could be on separate ports, (sub)domain names, or IPs. With this
+setup, you can either:
+
+1. Implement your own logic that will direct each upload to a specific Companion
+   endpoint by setting the `companionUrl` option
+2. Setting the Companion option `COMPANION_SELF_ENDPOINT`. This option will
+   cause Companion to respond with a `i-am` HTTP header containing the value
+   from `COMPANION_SELF_ENDPOINT`. When Uppy’s sees this header, it will pin all
+   requests for the upload to this endpoint.
+
+In either case, you would then also typically configure a single Companion
+instance (one endpoint) to handle all OAuth authentication requests, so that you
+only need to specify a single OAuth callback URL. See also `oauthDomain` and
+`validHosts`.
+
+#### Using a load balancer
+
+The other option is to set up a load balancer in front of many Companion
+instances. Then Uppy will only see a single endpoint and send all requests to
+the associated load balancer, which will then distribute them between Companion
+instances. The companion instances coordinate their messages and events over
+Redis so that any instance can serve the client’s requests. Note that sticky
+sessions are **not** needed with this setup. Here are the requirements for this
+setup:
+
+* The instances need to be connected to the same Redis server.
+* You need to set `COMPANION_SECRET` to the same value on both servers.
+* if you use the `companionKeysParams` feature (Transloadit), you also need
+  `COMPANION_PREAUTH_SECRET` to be the same on each instance.
+* All other configuration needs to be the same, except if you’re running many
+  instances on the same machine, then `COMPANION_PORT` should be different for
+  each instance.
+
+## API
+
+### Options
+
+:::tip
+
+The headings display the JS and environment variable options (`option`
+`ENV_OPTION`). When integrating Companion into your own server, you pass the
+options to `companion.app()`. If you are using the standalone version, you
+configure Companion using environment variables. Some options only exist as
+environment variables or only as a JS option.
+
+:::
+
+<details>
+  <summary>Default configuration</summary>
+
+```javascript
+const options = {
+	server: {
+		protocol: 'http',
+		path: '',
+	},
+	providerOptions: {},
+	s3: {
+		endpoint: 'https://{service}.{region}.amazonaws.com',
+		conditions: [],
+		useAccelerateEndpoint: false,
+		getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`,
+		expires: 800, // seconds
+	},
+	allowLocalUrls: false,
+	logClientVersion: true,
+	periodicPingUrls: [],
+	streamingUpload: false,
+	clientSocketConnectTimeout: 60000,
+	metrics: true,
+};
+```
+
+</details>
+
+#### `filePath` `COMPANION_DATADIR`
+
+Full path to the directory to which provider files will be downloaded
+temporarily.
+
+#### `secret` `COMPANION_SECRET` `COMPANION_SECRET_FILE`
+
+A secret string which Companion uses to generate authorization tokens. You
+should generate a long random string for this. For example:
+
+```js
+const crypto = require('node:crypto');
+
+const secret = crypto.randomBytes(64).toString('hex');
+```
+
+:::caution
+
+Omitting the `secret` in the standalone version will generate a secret for you,
+using the above `crypto` string. But when integrating with Express you must
+provide it yourself. This is an essential security measure.
+
+:::
+
+:::note
+
+Using a secret file means passing an absolute path to a file with any extension,
+which has only the secret, nothing else.
+
+:::
+
+#### `preAuthSecret` `COMPANION_PREAUTH_SECRET` `COMPANION_PREAUTH_SECRET_FILE`
+
+If you are using the [Transloadit](/docs/transloadit) `companionKeysParams`
+feature (Transloadit-hosted Companion using your own custom OAuth credentials),
+set this variable to a strong randomly generated secret. See also
+`COMPANION_SECRET` (but do not use the same secret!)
+
+:::note
+
+Using a secret file means passing an absolute path to a file with any extension,
+which has only the secret, nothing else.
+
+:::
+
+#### `uploadUrls` `COMPANION_UPLOAD_URLS`
+
+An allowlist (array) of strings (exact URLs) or regular expressions. Companion
+will only accept uploads to these URLs. This ensures that your Companion
+instance is only allowed to upload to your trusted servers and prevents
+[SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
+
+#### `COMPANION_PORT`
+
+The port on which to start the standalone server, defaults to 3020. This is a
+standalone-only option.
+
+#### `COMPANION_COOKIE_DOMAIN`
+
+Allows you to customize the domain of the cookies created for Express sessions.
+This is a standalone-only option.
+
+#### `COMPANION_HIDE_WELCOME`
+
+Setting this to `true` disables the welcome message shown at `/`. This is a
+standalone-only option.
+
+#### `redisUrl` `COMPANION_REDIS_URL`
+
+URL to running Redis server. This can be used to scale Companion horizontally
+using many instances. See [How to scale Companion](#how-to-scale-companion).
+
+#### `COMPANION_REDIS_EXPRESS_SESSION_PREFIX`
+
+Set a custom prefix for redis keys created by
+[connect-redis](https://github.com/tj/connect-redis). Defaults to `sess:`.
+Sessions are used for storing authentication state and for allowing thumbnails
+to be loaded by the browser via Companion. You might want to change this because
+if you run a redis with many different apps in the same redis server, it’s hard
+to know where `sess:` comes from and it might collide with other apps. **Note:**
+in the future, we plan and changing the default to `companion:` and possibly
+remove this option. This is a standalone-only option. See also
+`COMPANION_REDIS_PUBSUB_SCOPE`.
+
+#### `redisOptions`
+
+An object of
+[options supported by redis client](https://www.npmjs.com/package/redis#options-object-properties).
+This option can be used in place of `redisUrl`.
+
+#### `redisPubSubScope` `COMPANION_REDIS_PUBSUB_SCOPE`
+
+Use a scope for the companion events at the Redis server. Setting this option
+will prefix all events with the name provided and a colon. See also
+`COMPANION_REDIS_EXPRESS_SESSION_PREFIX`.
+
+#### `server`
+
+Configuration options for the underlying server.
+
+| Key / Environment variable               | Value             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                  |
+| ---------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `protocol` `COMPANION_PROTOCOL`          | `http` or `https` | Used to build a URL to reference the Companion instance itself, which is used for headers and cookies. Companion itself always runs as a HTTP server, so locally you should use `http`. You must to set this to `https` once you enabled SSL/HTTPS for your domain in production by running a reverse https-proxy in front of Companion, or with a built-in HTTPS feature of your hosting service.                                           |
+| `host` `COMPANION_DOMAIN`                | `String`          | Your server’s publicly facing hostname (for example `example.com`).                                                                                                                                                                                                                                                                                                                                                                          |
+| `oauthDomain` `COMPANION_OAUTH_DOMAIN`   | `String`          | If you have several instances of Companion with different (and perhaps dynamic) subdomains, you can set a single fixed subdomain and server (such as `sub1.example.com`) to handle your OAuth authentication for you. This would then redirect back to the correct instance with the required credentials on completion. This way you only need to configure a single callback URL for OAuth providers.                                      |
+| `path` `COMPANION_PATH`                  | `String`          | The server path to where the Companion app is sitting. For instance, if Companion is at `example.com/companion`, then the path would be `/companion`).                                                                                                                                                                                                                                                                                       |
+| `implicitPath` `COMPANION_IMPLICIT_PATH` | `String`          | If the URL’s path in your reverse proxy is different from your Companion path in your express app, then you need to set this path as `implicitPath`. For instance, if your Companion URL is `example.com/mypath/companion`. Where the path `/mypath` is defined in your NGINX server, while `/companion` is set in your express app. Then you need to set the option `implicitPath` to `/mypath`, and set the `path` option to `/companion`. |
+| `validHosts` `COMPANION_DOMAINS`         | `Array`           | If you are setting an `oauthDomain`, you need to set a list of valid hosts, so the oauth handler can validate the host of the Uppy instance requesting the authentication. This is essentially a list of valid domains running your Companion instances. The list may also contain regex patterns. e.g `['sub2.example.com', 'sub3.example.com', '(\\w+).example.com']`                                                                      |
+
+#### `sendSelfEndpoint` `COMPANION_SELF_ENDPOINT`
+
+This is essentially the same as the `server.host + server.path` attributes. The
+major reason for this attribute is that, when set, it adds the value as the
+`i-am` header of every request response.
+
+#### `providerOptions`
+
+Object to enable providers with their keys and secrets. For example:
+
+```json
+{
+	"drive": {
+		"key": "***",
+		"secret": "***"
+	}
+}
+```
+
+When using the standalone version you use the corresponding environment
+variables or point to a secret file (such as `COMPANION_GOOGLE_SECRET_FILE`).
+
+:::note
+
+Secret files need an absolute path to a file with any extension which only has
+the secret, nothing else.
+
+:::
+
+| Service      | Key         | Environment variables                                                                                                                                                                                                                  |
+| ------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Box          | `box`       | `COMPANION_BOX_KEY`, `COMPANION_BOX_SECRET`, `COMPANION_BOX_SECRET_FILE`                                                                                                                                                               |
+| Dropbox      | `dropbox`   | `COMPANION_DROPBOX_KEY`, `COMPANION_DROPBOX_SECRET`, `COMPANION_DROPBOX_SECRET_FILE`                                                                                                                                                   |
+| Facebook     | `facebook`  | `COMPANION_FACEBOOK_KEY`, `COMPANION_FACEBOOK_SECRET`, `COMPANION_FACEBOOK_SECRET_FILE`                                                                                                                                                |
+| Google Drive | `drive`     | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE`                                                                                                                                                      |
+| Instagram    | `instagram` | `COMPANION_INSTAGRAM_KEY`, `COMPANION_INSTAGRAM_SECRET`, `COMPANION_INSTAGRAM_SECRET_FILE`                                                                                                                                             |
+| OneDrive     | `onedrive`  | `COMPANION_ONEDRIVE_KEY`, `COMPANION_ONEDRIVE_SECRET`, `COMPANION_ONEDRIVE_SECRET_FILE`, `COMPANION_ONEDRIVE_DOMAIN_VALIDATION` (Settings this variable to `true` enables a route that can be used to validate your app with OneDrive) |
+| Zoom         | `zoom`      | `COMPANION_ZOOM_KEY`, `COMPANION_ZOOM_SECRET`, `COMPANION_ZOOM_SECRET_FILE`, `COMPANION_ZOOM_VERIFICATION_TOKEN`                                                                                                                       |
+
+#### `s3`
+
+Companion comes with signature endpoints for AWS S3. These can be used by the
+Uppy client to sign requests to upload files directly to S3, without exposing
+secret S3 keys in the browser. Companion also supports uploading files from
+providers like Dropbox and Instagram directly into S3.
+
+##### `s3.key` `COMPANION_AWS_KEY`
+
+The S3 access key ID.
+
+##### `s3.secret` `COMPANION_AWS_SECRET` `COMPANION_AWS_SECRET_FILE`
+
+The S3 secret access key.
+
+:::note
+
+Using a secret file means passing an absolute path to a file with any extension,
+which has only the secret, nothing else.
+
+:::
+
+##### `s3.endpoint` `COMPANION_AWS_ENDPOINT`
+
+Optional URL to a custom S3 (compatible) service. Otherwise uses the default
+from the AWS SDK.
+
+##### `s3.bucket` `COMPANION_AWS_BUCKET`
+
+The name of the bucket to store uploaded files in.
+
+It can be function that returns the name of the bucket as a `string` and takes
+the following arguments:
+
+* [`http.IncomingMessage`][], the HTTP request (will be `null` for remote
+  uploads)
+* metadata provided by the user for the file (will be `undefined` for local
+  uploads)
+
+##### `s3.region` `COMPANION_AWS_REGION`
+
+The datacenter region where the target bucket is located.
+
+##### `COMPANION_AWS_PREFIX`
+
+An optional prefix for all uploaded keys. This is a standalone-only option. The
+same can be achieved by the `getKey` option when using the express middleware.
+
+##### `s3.awsClientOptions`
+
+You can supply any
+[S3 option supported by the AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property)
+in the `providerOptions.s3.awsClientOptions` object, _except for_ the below:
+
+* `accessKeyId`. Instead, use the `providerOptions.s3.key` property. This is to
+  make configuration names consistent between different Companion features.
+* `secretAccessKey`. Instead, use the `providerOptions.s3.secret` property. This
+  is to make configuration names consistent between different Companion
+  features.
+
+Be aware that some options may cause wrong behaviour if they conflict with
+Companion’s assumptions. If you find that a particular option does not work as
+expected, please
+[open an issue on the Uppy repository](https://github.com/transloadit/uppy/issues/new)
+so we can document it here.
+
+##### `s3.getKey(req, filename, metadata)`
+
+Get the key name for a file. The key is the file path to which the file will be
+uploaded in your bucket. This option should be a function receiving three
+arguments:
+
+* `req` [`http.IncomingMessage`][], the HTTP request, for _regular_ S3 uploads
+  using the `@uppy/aws-s3` plugin. This parameter is _not_ available for
+  multipart uploads using the `@uppy/aws-s3` or `@uppy/aws-s3-multipart`
+  plugins. This parameter is `null` for remote uploads.
+* `filename`, the original name of the uploaded file;
+* `metadata`, user-provided metadata for the file.
+
+This function should return a string `key`. The `req` parameter can be used to
+upload to a user-specific folder in your bucket, for example:
+
+```js
+app.use(authenticationMiddleware);
+app.use(
+	uppy.app({
+		providerOptions: {
+			s3: {
+				getKey: (req, filename, metadata) => `${req.user.id}/${filename}`,
+				/* auth options */
+			},
+		},
+	}),
+);
+```
+
+The default implementation returns the `filename`, so all files will be uploaded
+to the root of the bucket as their original file name.
+
+```js
+app.use(
+	uppy.app({
+		providerOptions: {
+			s3: {
+				getKey: (req, filename, metadata) => filename,
+			},
+		},
+	}),
+);
+```
+
+When signing on the client, this function will only be called for multipart
+uploads.
+
+#### `COMPANION_AWS_USE_ACCELERATE_ENDPOINT`
+
+Enable S3
+[Transfer Acceleration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html).
+This is a standalone-only option.
+
+#### `COMPANION_AWS_EXPIRES`
+
+Set `X-Amz-Expires` query parameter in the presigned urls (in seconds, default:
+300\). This is a standalone-only option.
+
+#### `COMPANION_AWS_ACL`
+
+Set a
+[Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl)
+for uploaded objects. This is a standalone-only option.
+
+#### `customProviders`
+
+This option enables you to add custom providers along with the already supported
+providers. See [adding custom providers](#how-to-add-custom-providers) for more
+information.
+
+#### `logClientVersion`
+
+A boolean flag to tell Companion whether to log its version upon startup.
+
+#### `metrics` `COMPANION_HIDE_METRICS`
+
+A boolean flag to tell Companion whether to provide an endpoint `/metrics` with
+Prometheus metrics (by default metrics are enabled.)
+
+#### `streamingUpload` `COMPANION_STREAMING_UPLOAD`
+
+A boolean flag to tell Companion whether to enable streaming uploads. If
+enabled, it will lead to _faster uploads_ because companion will start uploading
+at the same time as downloading using `stream.pipe`. If `false`, files will be
+fully downloaded first, then uploaded. Defaults to `false`, but we recommended
+enabling it, especially if you’re expecting to upload large files. In future
+versions the default might change to `true`.
+
+#### `maxFileSize` `COMPANION_MAX_FILE_SIZE`
+
+If this value is set, companion will limit the maximum file size to process. If
+unset, it will process files without any size limit (this is the default).
+
+#### `periodicPingUrls` `COMPANION_PERIODIC_PING_URLS`
+
+If this value is set, companion will periodically send POST requests to the
+specified URLs. Useful for keeping track of companion instances as a keep-alive.
+
+#### `periodicPingInterval` `COMPANION_PERIODIC_PING_INTERVAL`
+
+Interval for periodic ping requests (in ms).
+
+#### `periodicPingStaticPayload` `COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD`
+
+A `JSON.stringify`-able JavaScript Object that will be sent as part of the JSON
+body in the period ping requests.
+
+#### `allowLocalUrls` `COMPANION_ALLOW_LOCAL_URLS`
+
+A boolean flag to tell Companion whether to allow requesting local URLs
+(non-internet IPs).
+
+:::caution
+
+Only enable this in development. **Enabling it in production is a security
+risk.**
+
+:::
+
+#### `corsOrigins` `COMPANION_CLIENT_ORIGINS`
+
+Allowed CORS Origins (default `true`). Passed as the `origin` option in
+[cors](https://github.com/expressjs/cors#configuration-options))
+
+#### `COMPANION_CLIENT_ORIGINS_REGEX`
+
+Like COMPANION\_CLIENT\_ORIGINS, but allows a single regex instead.
+`COMPANION_CLIENT_ORIGINS` will be ignored if this is used. This is a
+standalone-only option.
+
+#### `chunkSize` `COMPANION_CHUNK_SIZE`
+
+Controls how big the uploaded chunks are for AWS S3 Multipart and Tus. Smaller
+values lead to more overhead, but larger values lead to slower retries in case
+of bad network connections. Passed to tus-js-client
+[`chunkSize`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#chunksize)
+as well as
+[AWS S3 Multipart](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
+`partSize`.
+
+#### `enableUrlEndpoint` `COMPANION_ENABLE_URL_ENDPOINT`
+
+Set this to `false` to disable the
+[URL functionalily](https://uppy.io/docs/url/). Default: `true`.
+
+### Events
+
+The object returned by `companion.app()` also has a property `companionEmitter`
+which is an `EventEmitter` that emits the following events:
+
+* `upload-start` - When an upload starts, this event is emitted with an object
+  containing the property `token`, which is a unique ID for the upload.
+* **token** - The event name is the token from `upload-start`. The event has an
+  object with the following properties:
+  * `action` - One of the following strings:
+    * `success` - When the upload succeeds.
+    * `error` - When the upload fails with an error.
+  * `payload` - the error or success payload.
+
+Example code for using the `EventEmitter` to handle a finished file upload:
+
+```js
+const companionApp = companion.app(options);
+const { companionEmitter: emitter } = companionApp;
+
+emitter.on('upload-start', ({ token }) => {
+	console.log('Upload started', token);
+
+	function onUploadEvent({ action, payload }) {
+		if (action === 'success') {
+			emitter.off(token, onUploadEvent); // avoid listener leak
+			console.log('Upload finished', token, payload.url);
+		} else if (action === 'error') {
+			emitter.off(token, onUploadEvent); // avoid listener leak
+			console.error('Upload failed', payload);
+		}
+	}
+	emitter.on(token, onUploadEvent);
+});
+```
+
+<!--retext-simplify ignore frequently-->
+
+## Frequently asked questions
+
+### Do you have a live example?
+
+An example server is running at <https://companion.uppy.io>.
+
+### How does the Authentication and Token mechanism work?
+
+This section describes how Authentication works between Companion and Providers.
+While this behaviour is the same for all Providers (Dropbox, Instagram, Google
+Drive, etc.), we are going to be referring to Dropbox in place of any Provider
+throughout this section.
+
+The following steps describe the actions that take place when a user
+Authenticates and Uploads from Dropbox through Companion:
+
+* The visitor to a website with Uppy clicks `Connect to Dropbox`.
+* Uppy sends a request to Companion, which in turn sends an OAuth request to
+  Dropbox (Requires that OAuth credentials from Dropbox have been added to
+  Companion).
+* Dropbox asks the visitor to log in, and whether the Website should be allowed
+  to access your files
+* If the visitor agrees, Companion will receive a token from Dropbox, with which
+  we can temporarily download files.
+* Companion encrypts the token with a secret key and sends the encrypted token
+  to Uppy (client)
+* Every time the visitor clicks on a folder in Uppy, it asks Companion for the
+  new list of files, with this question, the token (still encrypted by
+  Companion) is sent along.
+* Companion decrypts the token, requests the list of files from Dropbox and
+  sends it to Uppy.
+* When a file is selected for upload, Companion receives the token again
+  according to this procedure, decrypts it again, and thereby downloads the file
+  from Dropbox.
+* As the bytes arrive, Companion uploads the bytes to the final destination
+  (depending on the configuration: Apache, a Tus server, S3 bucket, etc).
+* Companion reports progress to Uppy, as if it were a local upload.
+* Completed!
+
+### How to use provider redirect URIs?
+
+When generating your provider API keys on their corresponding developer
+platforms (e.g
+[Google Developer Console](https://console.developers.google.com/)), you’d need
+to provide a `redirect URI` for the OAuth authorization process. In general the
+redirect URI for each provider takes the format:
+
+`http(s)://$YOUR_COMPANION_HOST_NAME/$PROVIDER_NAME/redirect`
+
+For example, if your Companion server is hosted on
+`https://my.companion.server.com`, then the redirect URI you would supply for
+your OneDrive provider would be:
+
+`https://my.companion.server.com/onedrive/redirect`
+
+Please see
+[Supported Providers](https://uppy.io/docs/companion/#Supported-providers) for a
+list of all Providers and their corresponding names.
+
+### How to use Companion with Kubernetes?
+
+We have a detailed
+[guide](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/companion/KUBERNETES.md)
+on running Companion in Kubernetes.
+
+### How to add custom providers?
+
+As of now, Companion supports the
+[providers listed here](https://uppy.io/docs/companion/#Supported-providers) out
+of the box, but you may also choose to add your own custom providers. You can do
+this by passing the `customProviders` option when calling the Uppy `app` method.
+The custom provider is expected to support Oauth 1 or 2 for
+authentication/authorization.
+
+```javascript
+import providerModule from './path/to/provider/module';
+
+const options = {
+	customProviders: {
+		myprovidername: {
+			config: {
+				authorize_url: 'https://mywebsite.com/authorize',
+				access_url: 'https://mywebsite.com/token',
+				oauth: 2,
+				key: '***',
+				secret: '***',
+				scope: ['read', 'write'],
+			},
+			module: providerModule,
+		},
+	},
+};
+
+uppy.app(options);
+```
+
+The `customProviders` option should be an object containing each custom
+provider. Each custom provider would, in turn, be an object with two keys,
+`config` and `module`. The `config` option would contain Oauth API settings,
+while the `module` would point to the provider module.
+
+To work well with Companion, the **module** must be a class with the following
+methods. Note that the methods must be `async`, return a `Promise` or reject
+with an `Error`):
+
+1. `async list ({ token, directory, query })` - Returns a object containing a
+   list of user files (such as a list of all the files in a particular
+   directory). See [example returned list data structure](#list-data). `token` -
+   authorization token (retrieved from oauth process) to send along with your
+   request
+   * `directory` - the id/name of the directory from which data is to be
+     retrieved. This may be ignored if it doesn’t apply to your provider
+   * `query` - expressjs query params object received by the server (in case
+     some data you need in there).
+2. `async download ({ token, id, query })` - Downloads a particular file from
+   the provider. Returns an object with a single property `{ stream }` - a
+   [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable),
+   which will be read from and uploaded to the destination. To prevent memory
+   leaks, make sure you release your stream if you reject this method with an
+   error.
+   * `token` - authorization token (retrieved from oauth process) to send along
+     with your request.
+   * `id` - ID of the file being downloaded.
+   * `query` - expressjs query params object received by the server (in case
+     some data you need in there).
+3. `async size ({ token, id, query })` - Returns the byte size of the file that
+   needs to be downloaded as a `Number`. If the size of the object is not known,
+   `null` may be returned.
+   * `token` - authorization token (retrieved from oauth process) to send along
+     with your request.
+   * `id` - ID of the file being downloaded.
+   * `query` - expressjs query params object received by the server (in case
+     some data you need in there).
+
+The class must also have:
+
+* A unique `static authProvider` string property - a lowercased value which
+  indicates name of the [`grant`](https://github.com/simov/grant) OAuth2
+  provider to use (e.g `google` for Google). If your provider doesn’t use
+  OAuth2, you can omit this property.
+* A `static` property `static version = 2`, which is the current version of the
+  Companion Provider API.
+
+See also
+[example code with a custom provider](https://github.com/transloadit/uppy/blob/main/examples/custom-provider/server).
+
+#### list data
+
+```json
+{
+	// username or email of the user whose provider account is being accessed
+	"username": "johndoe",
+	// list of files and folders in the directory. An item is considered a folder
+	//  if it mainly exists as a collection to contain sub-items
+	"items": [
+		{
+			// boolean value of whether or NOT it's a folder
+			"isFolder": false,
+			// icon image URL
+			"icon": "https://random-api.url.com/fileicon.jpg",
+			// name of the item
+			"name": "myfile.jpg",
+			// the mime type of the item. Only relevant if the item is NOT a folder
+			"mimeType": "image/jpg",
+			// the id (in string) of the item
+			"id": "uniqueitemid",
+			// thumbnail image URL. Only relevant if the item is NOT a folder
+			"thumbnail": "https://random-api.url.com/filethumbnail.jpg",
+			// for folders this is typically the value that will be passed as "directory" in the list(...) method.
+			// For files, this is the value that will be passed as id in the download(...) method.
+			"requestPath": "file-or-folder-requestpath",
+			// datetime string (in ISO 8601 format) of when this item was last modified
+			"modifiedDate": "2020-06-29T19:59:58Z",
+			// the size in bytes of the item. Only relevant if the item is NOT a folder
+			"size": 278940,
+			"custom": {
+				// an object that may contain some more custom fields that you may need to send to the client. Only add this object if you have a need for it.
+				"customData1": "the value",
+				"customData2": "the value"
+			}
+			// more items here
+		}
+	],
+	// if the "items" list is paginated, this is the request path needed to fetch the next page.
+	"nextPagePath": "directory-name?cursor=cursor-to-next-page"
+}
+```
+
+### How to run Companion locally?
+
+1. To set up Companion for local development, please clone the Uppy repo and
+   install, like so:
+
+   ```bash
+   git clone https://github.com/transloadit/uppy
+   cd uppy
+   yarn install
+   ```
+
+2. Configure your environment variables by copying the `env.example.sh` file to
+   `env.sh` and edit it to its correct values.
+
+   ```bash
+   cp .env.example .env
+   $EDITOR .env
+   ```
+
+3. To start the server, run:
+
+   ```bash
+   yarn run start:companion
+   ```
+
+This would get the Companion instance running on `http://localhost:3020`. It
+uses [nodemon](https://github.com/remy/nodemon) so it will automatically restart
+when files are changed.
+
+[`http.incomingmessage`]: https://nodejs.org/api/http.html#class-httpincomingmessage
+
+[box]: /docs/box
+
+[dropbox]: /docs/dropbox
+
+[facebook]: /docs/facebook
+
+[googledrive]: /docs/google-drive
+
+[instagram]: /docs/instagram
+
+[onedrive]: /docs/onedrive
+
+[unsplash]: /docs/unsplash
+
+[url]: /docs/url
+
+[zoom]: /docs/zoom
+
+[transloadit]: https://transloadit.com

+ 125 - 0
docs/compressor.mdx

@@ -0,0 +1,125 @@
+---
+sidebar_position: 12
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Compressor
+
+The `@uppy/compressor` plugin optimizes images (JPEG, PNG, and any other format
+supported by the client’s browser) before upload, saving up to 60% in size
+(roughly 18 MB for 10 images). It uses [Compressor.js][] library under the hood.
+
+## When should I use it?
+
+When your users are likely to upload images, potentially on mobile devices, and
+saving data and faster uploads are important.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/compressor
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/compressor
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Compressor } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Compressor, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {7} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Compressor from '@uppy/compressor';
+
+new Uppy()
+  .use(Dashboard, {inline:true, target: '#dashboard')
+  .use(Compressor);
+```
+
+No action is needed from the user — Uppy will automatically optimize images,
+show an [Informer](/docs/informer) message with saved bytes, and then begin the
+upload as usual.
+
+## API
+
+### Options
+
+:::tip
+
+You can also pass any of the [Compressor.js options][] here as well.
+
+:::
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Compressor'`).
+
+#### `quality`
+
+The quality of the output image passed to [Compressor.js][] (`number`, default:
+`0.6`).
+
+It must be a number between `0` and `1`. Be careful to use `1` as it may make
+the size of the output image become larger. In most cases, going with the
+default value is best.
+
+:::note
+
+This option is only available for `image/jpeg` and `image/webp` images.
+
+:::
+
+#### `limit`
+
+Number of images that will be compressed in parallel (`number`, default: `10`).
+
+You likely don’t need to change this, unless you are experiencing performance
+issues.
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		// Shown in the Status Bar
+		compressingImages: 'Compressing images...',
+		compressedX: 'Saved %{size} by compressing images',
+	},
+};
+```
+
+## Events
+
+#### `compressor:complete`
+
+The event is emitted when all files are compressed. You can use it for side
+effects or custom UI notifications.
+
+[compressor.js]: https://github.com/fengyuanchen/compressorjs
+[compressor.js options]: https://github.com/fengyuanchen/compressorjs#options

+ 181 - 0
docs/form.mdx

@@ -0,0 +1,181 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Form
+
+The `@uppy/form` plugin integrates with an existing HTML `<form>` element to
+extract input data from it, and send along with the Uppy upload. It then appends
+the upload result back to the form via a hidden input.
+
+## When should I use this?
+
+When you have an existing HTML `<form>` element and you would like to:
+
+- **Attach the form input values to the files.** This is useful if you want to
+  associate meta data from the form (for example, name, location, id) with the
+  uploaded file, so you can process it on the backend. `@uppy/form` extracts the
+  input values before uploading/processing files and adds them to Uppy meta data
+  state (`uppy.state.meta`), as well as and each file’s meta, and appends to the
+  upload in an object with `[file input name attribute]` -> `[file input value]`
+  key/values.
+- **Upload the files and put the response (such as the file URLs) into a hidden
+  field** (`<input type="hidden" name="uppyResult">`). Then you can POST and
+  handle the form yourself. The appended result is a stringified version of a
+  result returned from calling `uppy.upload()` or listening to `complete` event.
+- **Automatically start the file upload on submit or submit the form after file
+  upload.** This is off by default. See [`submitOnSuccess`](#submitOnSuccess)
+  and [`triggerUploadOnSubmit`](#triggerUploadOnSubmit) options respectively for
+  details.
+
+:::note
+
+If you are using a UI framework or library like React, Vue or Svelte, you’ll
+most likely handle form data there as well, and thus won’t need this plugin.
+Instead, pass meta data to Uppy via [`uppy.setMeta()`](/docs/uppy#setmetadata)
+and listen to [`uppy.on('complete')`](/docs/uppy#complete) to get the upload
+results back.
+
+:::
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/form
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/form
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Form } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Form, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js title="app.js"
+import Uppy from '@uppy/core';
+import Form from '@uppy/form';
+import DragDrop from '@uppy/drag-drop';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/drag-drop/dist/style.min.css';
+
+new Uppy().use(Form, {
+	target: '#my-form',
+});
+```
+
+```html title="index.html"
+<form id="my-form" action="/submit" method="get">
+	<label for="name">Enter your name: </label>
+	<input type="text" name="name" required />
+
+	<label for="dob">Date of birth: </label>
+	<input type="date" name="dob" />
+
+	<input type="submit" value="Save" />
+</form>
+```
+
+By default the code above will:
+
+1. Extract meta data from the form `#my-form`.
+2. Send it with the Uppy upload.
+3. Those fields will then be added to Uppy meta data state (`uppy.state.meta`)
+   and each file’s meta, and appended as (meta)data to the upload in an object
+   with `[file input name attribute]` -> `[file input value]` key/values.
+4. When Uppy completes upload/processing, it will add an
+   `<input type="hidden" name="uppyResult">` with the stringified upload result
+   object back to the form.
+
+:::note
+
+You can disable both of these features, see options below.
+
+:::
+
+:::tip
+
+`@uppy/form` can also start Uppy upload automatically once the form is
+submitted, and even submit the form after the upload is complete. This is off by
+default. See [`triggerUploadOnSubmit`](#triggerUploadOnSubmit) and
+[`submitOnSuccess`](#submitOnSuccess) options below for details.
+
+:::
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Form'`).
+
+#### `target`
+
+DOM element or CSS selector for the form element (`string` or `Element`,
+default: `null`).
+
+This is required for the plugin to work.
+
+#### `resultName`
+
+The `name` attribute for the `<input type="hidden">` where the result will be
+added (`string`, default: `uppyResult`).
+
+#### `getMetaFromForm`
+
+Configures whether to extract metadata from the form (`boolean`, default:
+`true`).
+
+When set to `true`, the Form plugin will extract all fields from a `<form>`
+element before upload begins. Those fields will then be added to Uppy meta data
+state (`uppy.state.meta`) and each file’s meta, and appended as (meta)data to
+the upload in an object with `[file input name attribute]` ->
+`[file input value]` key/values.
+
+#### `addResultToForm`
+
+Configures whether to add upload/encoding results back to the form in an
+`<input name="uppyResult" type="hidden">` element (`boolean`, default: `true`).
+
+#### `triggerUploadOnSubmit`
+
+Configures whether to start the upload when the form is submitted (`boolean`,
+default: `false`).
+
+When a user submits the form (via a submit button, the <kbd>Enter</kbd> key or
+otherwise), this option will prevent form submission, and instead upload files
+via Uppy. Then you could:
+
+- Set `submitOnSuccess: true` if you need the form to _actually_ be submitted
+  once all files have been uploaded.
+- Listen for `uppy.on('complete')` event to do something else if the file
+  uploads are all you need. For example, if the form is used for file metadata
+  only.
+
+#### `submitOnSuccess`
+
+Configures whether to submit the form after Uppy finishes uploading/encoding
+(`boolean`, default: `false`).

+ 4 - 0
docs/framework-integrations/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "Framework integrations",
+	"position": 8
+}

+ 108 - 0
docs/framework-integrations/angular.mdx

@@ -0,0 +1,108 @@
+---
+slug: /angular
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Angular
+
+[Angular][] components for Uppy UI plugins.
+
+## When should I use it?
+
+When you are using the Angular framework and you wouldl like to use on of the UI
+components.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/angular
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/angular
+```
+
+  </TabItem>
+</Tabs>
+
+:::note
+
+You also need to install the UI plugin you want to use. For instance,
+`@uppy/dashboard`.
+
+:::
+
+## Use
+
+Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
+instance can be passed into components as a `props` prop.
+
+The following plugins are available as Angular component wrappers:
+
+- `<uppy-dashboard />` renders [`@uppy/dashboard`](/docs/dashboard)
+- `<uppy-drag-drop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
+- `<uppy-progress-bar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
+- `<uppy-status-bar />` renders [`@uppy/status-bar`](/docs/status-bar)
+
+Each component takes a `props` prop that will be passed to the UI Plugin.
+
+```typescript title="app.module.ts" showLineNumbers
+import { NgModule } from '@angular/core';
+import { UppyAngularDashboardModule } from '@uppy/angular';
+
+import { BrowserModule } from '@angular/platform-browser';
+import { AppComponent } from './app.component';
+
+@NgModule({
+	declarations: [AppComponent],
+	imports: [BrowserModule, UppyAngularDashboardModule],
+	providers: [],
+	bootstrap: [AppComponent],
+})
+class {}
+```
+
+```html title="app.component.html" showLineNumbers
+<uppy-dashboard [uppy]="uppy"> </uppy-dashboard>
+```
+
+You should initialize Uppy as a property of your component.
+
+```typescript title="app.component.ts" showLineNumbers
+import { Component } from '@angular/core';
+import { Uppy } from '@uppy/core';
+
+@Component({
+	selector: 'app-root',
+})
+export class AppComponent {
+	uppy: Uppy = new Uppy({ debug: true, autoProceed: true });
+}
+```
+
+### CSS
+
+All components have their own styling and should be added to your component
+decorator. You can find the CSS import statements in the docs of the UI plugin
+you want to use. For instance, for `@uppy/dashboard`:
+
+```typescript
+@Component({
+  // ...
+  styleUrls: [
+    '../node_modules/@uppy/core/dist/style.css',
+    '../node_modules/@uppy/dashboard/dist/style.css',
+  ],
+})
+```
+
+[angular]: https://angular.io

+ 228 - 0
docs/framework-integrations/react.mdx

@@ -0,0 +1,228 @@
+---
+slug: /react
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# React
+
+[React][] components for the Uppy UI plugins and a `useUppyState` hook.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/react
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/react
+```
+
+  </TabItem>
+</Tabs>
+
+:::note
+
+You also need to install the UI plugin you want to use. For instance,
+`@uppy/dashboard`.
+
+:::
+
+## Use
+
+`@uppy/react` exposes component wrappers for `Dashboard`, `DragDrop`, and all
+other UI elements. The components can be used with either [React][] or
+API-compatible alternatives such as [Preact][].
+
+:::caution
+
+If you find yourself writing many instances of `useState` and `useEffect` to
+achieve something with Uppy in React, you are most likely breaking React best
+practices. Consider reading
+“[You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)”
+and looking at our examples below.
+
+:::
+
+### Components
+
+The following components are exported from `@uppy/react`:
+
+- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard)
+- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
+- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
+- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
+
+{/* prettier-ignore */}
+{/* Commented out until the hook is live
+
+### Hooks
+
+`useUppyState(uppy, selector)`
+
+Use this hook when you need to access Uppy’s state reactively. Most of the
+times, this is needed if you are building a custom UI for Uppy in React.
+
+```js
+// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
+const [uppy] = useState(() => new Uppy());
+
+const files = useUppyState(uppy, (state) => state.files);
+const totalProgress = useUppyState(uppy, (state) => state.totalProgress);
+// We can also get specific plugin state.
+// Note that the value on `plugins` depends on the `id` of the plugin.
+const metaFields = useUppyState(
+	uppy,
+	(state) => state.plugins?.Dashboard?.metaFields,
+);
+```
+
+You can see all the values you can access on the
+[`State`](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/core/types/index.d.ts#L190)
+type. If you are accessing plugin state, you would have to look at the types of
+the plugin.
+
+\*/}
+
+## Examples
+
+### Example: basic component
+
+Here we have a basic component which ties Uppy’s state to the component. This
+means you can render multiple instances. But be aware that as your component
+unmounts, for instance because the user navigates to a different page, Uppy’s
+state will be lost and uploads will stop.
+
+:::note
+
+If you render multiple instances of Uppy, make sure to give each instance a
+unique `id`.
+
+:::
+
+```js
+import React, { useEffect, useState } from 'react';
+import Uppy from '@uppy/core';
+import Webcam from '@uppy/webcam';
+import { Dashboard } from '@uppy/react';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+import '@uppy/webcam/dist/style.min.css';
+
+function Component() {
+	// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
+	const [uppy] = useState(() => new Uppy().use(Webcam));
+
+	return <Dashboard uppy={uppy} plugins={['Webcam']} />;
+}
+```
+
+### Example: keep Uppy state and uploads while navigating between pages
+
+When you want Uppy’s state to persist and keep uploads running between pages,
+you can
+[lift the state up](https://react.dev/learn/sharing-state-between-components#lifting-state-up-by-example).
+
+```js
+import React, { useState, useEffect } from 'react';
+import Uppy from '@uppy/core';
+import { Dashboard } from '@uppy/react';
+
+function Page1() {
+	// ...
+}
+
+function Page2({ uppy }) {
+	return (
+		<>
+			<p>{totalProgress}</p>
+			<Dashboard id="dashboard" uppy={uppy} />
+		</>
+	);
+}
+
+export default function App() {
+	// keeping the uppy instance alive above the pages the user can switch during uploading
+	const [uppy] = useState(() => new Uppy());
+
+	return (
+		// Add your router here
+		<>
+			<Page1 />
+			<Page2 uppy={uppy} />
+		</>
+	);
+}
+```
+
+### Example: updating Uppy’s options dynamically based on props
+
+```js
+// ...
+function Component(props) {
+	// IMPORTANT: passing an initializer function to prevent the state from recreating.
+	const [uppy] = useState(() => new Uppy().use(Webcam));
+
+	useEffect(() => {
+		uppy.setOptions({ restrictions: props.restrictions });
+	}, [props.restrictions]);
+
+	useEffect(() => {
+		uppy.getPlugin('Webcam').setOptions({ modes: props.webcamModes });
+	}, [props.webcamModes]);
+
+	return <Dashboard uppy={uppy} plugins={['Webcam']} />;
+}
+```
+
+### Example: dynamic params and signature for Transloadit
+
+When you go to production always make sure to set the `signature`. **Not using
+[Signature Authentication](https://transloadit.com/docs/topics/signature-authentication/)
+can be a security risk**. Signature Authentication is a security measure that
+can prevent outsiders from tampering with your Assembly Instructions.
+
+Generating a signature should be done on the server to avoid leaking secrets. In
+React, this could get awkward with a `fetch` in a `useEffect` and setting it to
+`useState`. Instead, it’s easier to use the
+[`assemblyOptions`](/docs/transloadit#assemblyoptions) option to `fetch` the
+params.
+
+```js
+// ...
+function createUppy(userId) {
+	return new Uppy({ meta: { userId } }).use(Transloadit, {
+		async assemblyOptions(file) {
+			// You can send meta data along for use in your template.
+			// https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions
+			const body = JSON.stringify({ userId: file.meta.userId });
+			const res = await fetch('/transloadit-params', { method: 'POST', body });
+			return response.json();
+		},
+	});
+}
+
+function Component({ userId }) {
+	// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
+	const [uppy] = useState(() => createUppy(userId));
+
+	useEffect(() => {
+		if (userId) {
+			// Adding to global `meta` will add it to every file.
+			uppy.setOptions({ meta: { userId } });
+		}
+	}, [uppy, userId]);
+}
+```
+
+[react]: https://facebook.github.io/react
+[preact]: https://preactjs.com/

+ 70 - 0
docs/framework-integrations/svelte.mdx

@@ -0,0 +1,70 @@
+---
+slug: /svelte
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Svelte
+
+[Svelte][] components for the Uppy UI plugins.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/svelte
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/svelte
+```
+
+  </TabItem>
+</Tabs>
+
+:::note
+
+You also need to install the UI plugin you want to use. For instance,
+`@uppy/dashboard`.
+
+:::
+
+## Use
+
+The following plugins are available as Svelte component wrappers:
+
+- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard)
+- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
+- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
+- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
+
+Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
+instance can be passed into components as an `uppy` prop. Due to the way Svelte
+handles reactivity, you can initialize Uppy the same way you would with vanilla
+JavaScript.
+
+```html
+<script>
+	import { Dashboard } from '@uppy/svelte';
+	import Uppy from '@uppy/core';
+	import Webcam from '@uppy/webcam';
+
+	// Don't forget the CSS: core and UI components + plugins you are using
+	import '@uppy/core/dist/style.css';
+	import '@uppy/dashboard/dist/style.css';
+	import '@uppy/webcam/dist/style.css';
+
+	const uppy = new Uppy().use(Webcam);
+</script>
+
+<main><Dashboard uppy={uppy} plugins={["Webcam"]} /></main>
+```
+
+[svelte]: https://svelte.dev

+ 73 - 0
docs/framework-integrations/vue.mdx

@@ -0,0 +1,73 @@
+---
+slug: /vue
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Vue
+
+[Vue][] components for the Uppy UI plugins.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/vue
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/vue
+```
+
+  </TabItem>
+</Tabs>
+
+:::note
+
+You also need to install the UI plugin you want to use. For instance,
+`@uppy/dashboard`.
+
+:::
+
+## Use
+
+The following plugins are available as Vue component wrappers:
+
+- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard) inline
+- `<DashboardModal />` renders [`@uppy/dashboard`](/docs/dashboard) as a modal
+- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
+- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
+- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
+
+Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
+instance can be passed into components as an `uppy` prop. Due to the way Vue
+handles reactivity, you can initialize Uppy the same way you would with vanilla
+JavaScript.
+
+```html
+<script>
+	import { Dashboard } from '@uppy/vue';
+	import Uppy from '@uppy/core';
+	import Webcam from '@uppy/webcam';
+
+	// Don't forget the CSS: core and UI components + plugins you are using
+	import '@uppy/core/dist/style.css';
+	import '@uppy/dashboard/dist/style.css';
+	import '@uppy/webcam/dist/style.css';
+
+	const uppy = new Uppy().use(Webcam);
+</script>
+
+<template>
+	<Dashboard :uppy="uppy" :plugins="['Webcam']" />
+</template>
+```
+
+[vue]: https://vuejs.org

+ 150 - 0
docs/golden-retriever.mdx

@@ -0,0 +1,150 @@
+---
+sidebar_position: 11
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Golden Retriever
+
+The `@uppy/golden-retriever` plugin saves selected files in your browser cache,
+so that if the browser crashes, or the user accidentally closes the tab, Uppy
+can restore everything and continue uploading as if nothing happened. You can
+read more about it
+[on our blog](https://uppy.io/blog/2017/07/golden-retriever/).
+
+The Golden Retriever uses three methods of browser data storage:
+
+- `LocalStorage` to store file metadata and Uppy state only.
+- `IndexedDB` for small files, usually under 5MiB.
+- `Service Worker` (_optional_) for _all_ files because, unlike `IndexedDB`,
+  `Service Worker` can keep references to large files. Service Worker storage is
+  _quite_ temporary though, and doesn’t persist across browser crashes or
+  restarts. It works well, however, for accidental refreshes or closed tabs.
+
+Upon restore, the plugin first restores state from `LocalStorage` and then
+checks both file storages — `IndexedDB` and `ServiceWorker` — merging the
+results.
+
+If restore is unsuccessful for certain files, they will be marked as “ghosts” in
+the Dashboard UI, and a message + button offering to re-select those files will
+be displayed.
+
+Checkout the
+[storage quotas](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#other_web_technologies)
+on MDN to see how much data can be stored depending on the device and browser.
+
+## When should I use this?
+
+When you want extra insurance for the user-selected files. If you feel like
+users might spend some time picking files, tweaking descriptions etc, and will
+not appreciate having to do it over again if something crashes. Another use case
+might be when you think user might want to pick a few files, navigate away to do
+something else in your app or otherwise, and then come back to the same
+selection.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/golden-retriever
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/golden-retriever
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, GoldenRetriever } from "{{UPPY_JS_URL}}"
+        new Uppy().use(GoldenRetriever)
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {7} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import GoldenRetriever from '@uppy/golden-retriever';
+
+new Uppy()
+  .use(Dashboard, {inline:true, target: '#dashboard')
+  .use(GoldenRetriever);
+```
+
+By default, Golden Retriever will only use the `IndexedDB` storage, which is
+good enough for files up to 5 MiB. `Service Worker` is optional and requires
+setup.
+
+### Enabling Service Worker
+
+With the Service Worker storage, Golden Retriever will be able to temporary
+store references to large files.
+
+1. Bundle your own service worker `sw.js` file with Uppy GoldenRetriever’s
+   service worker.
+
+   :::tip
+
+   For Webpack, check out
+   [serviceworker-webpack-plugin](https://github.com/oliviertassinari/serviceworker-webpack-plugin).
+
+   :::
+
+   ```js title="sw.js"
+   import('@uppy/golden-retriever/lib/ServiceWorker');
+   ```
+
+2. Register it in your app’s entry point:
+
+   ```js
+   // you app.js entry point
+   uppy.use(GoldenRetriever, { serviceWorker: true });
+
+   if ('serviceWorker' in navigator) {
+   	navigator.serviceWorker
+   		.register('/sw.js') // path to your bundled service worker with GoldenRetriever service worker
+   		.then((registration) => {
+   			console.log(
+   				'ServiceWorker registration successful with scope: ',
+   				registration.scope,
+   			);
+   		})
+   		.catch((error) => {
+   			console.log(`Registration failed with ${error}`);
+   		});
+   }
+   ```
+
+Voilà, that’s it. Happy retrieving!
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'GoldenRetriever'`).
+
+#### `expires`
+
+How long to store metadata and files for. Used for `LocalStorage` and
+`IndexedDB` (`number`, default: `24 * 60 * 60 * 1000` // 24 hours).
+
+#### `serviceWorker`
+
+Whether to enable `Service Worker` storage (`boolean`, default: `false`).

+ 4 - 0
docs/guides/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "Guides",
+	"position": 2
+}

+ 62 - 0
docs/guides/browser-support.mdx

@@ -0,0 +1,62 @@
+---
+sidebar_position: 7
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Supporting IE11
+
+We officially support recent versions of Chrome, Firefox, Safari and Edge.
+
+Internet Explorer is not officially supported, as in we don’t run tests for it,
+but you can be mostly confident it works with the right polyfills. But it does
+come with a risk of unexpected results in styling or functionality.
+
+## Polyfills
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install core-js whatwg-fetch abortcontroller-polyfill md-gum-polyfill resize-observer-polyfill
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add core-js whatwg-fetch abortcontroller-polyfill md-gum-polyfill resize-observer-polyfill
+```
+
+  </TabItem>
+</Tabs>
+
+```js
+import 'core-js';
+import 'whatwg-fetch';
+import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
+// Order matters: AbortController needs fetch which needs Promise (provided by core-js).
+
+import 'md-gum-polyfill';
+import ResizeObserver from 'resize-observer-polyfill';
+
+window.ResizeObserver ??= ResizeObserver;
+
+export { default } from '@uppy/core';
+export * from '@uppy/core';
+```
+
+## Legacy CDN bundle
+
+<UppyCdnExample uppyJsName="uppy.legacy.min.js">
+	{`
+    import { Uppy, DragDrop, Tus } from "{{UPPY_JS_URL}}"
+    const uppy = new Uppy()
+    uppy.use(DragDrop, { target: '#uppy' })
+    uppy.use(Tus, { endpoint: '//tusd.tusdemo.net/files/' })
+  `}
+</UppyCdnExample>

+ 382 - 0
docs/guides/building-plugins.md

@@ -0,0 +1,382 @@
+---
+sidebar_position: 3
+---
+
+# Building plugins
+
+You can find already a few useful Uppy plugins out there, but there might come a
+time when you will want to build your own. Plugins can hook into the upload
+process or render a custom UI, typically to:
+
+* Render some custom UI element, such as [StatusBar](/docs/status-bar) or
+  [Dashboard](/docs/dashboard).
+* Do the actual uploading, such as [XHRUpload](/docs/xhr-upload) or
+  [Tus](/docs/tus).
+* Do work before the upload, like compressing an image or calling external API.
+* Interact with a third-party service to process uploads correctly, such as
+  [Transloadit](/docs/transloadit) or [AwsS3](/docs/aws-s3).
+
+See a [full example of a plugin](#example-of-a-custom-plugin) below.
+
+## Creating A Plugin
+
+Uppy has two classes to create plugins with. `BasePlugin` for plugins that don’t
+need a user interface, and `UIPlugin` for ones that do. Each plugin has an `id`
+and a `type`. `id`s are used to uniquely identify plugins. A `type` can be
+anything—some plugins use `type`s to decide whether to do something to some
+other plugin. For example, when targeting plugins at the built-in `Dashboard`
+plugin, the Dashboard uses the `type` to figure out where to mount different UI
+elements. `'acquirer'`-type plugins are mounted into the tab bar, while
+`'progressindicator'`-type plugins are mounted into the progress bar area.
+
+The plugin constructor receives the Uppy instance in the first parameter, and
+any options passed to `uppy.use()` in the second parameter.
+
+```js
+import BasePlugin from '@uppy/core/lib/BasePlugin.js';
+
+export default class MyPlugin extends BasePlugin {
+	constructor(uppy, opts) {
+		super(uppy, opts);
+		this.id = opts.id || 'MyPlugin';
+		this.type = 'example';
+	}
+}
+```
+
+## Methods
+
+Plugins can define methods to execute certain tasks. The most important method
+is `install()`, which is called when a plugin is `.use`d.
+
+All the below methods are optional! Only define the methods you need.
+
+### `BasePlugin`
+
+#### `install()`
+
+Called when the plugin is `.use`d. Do any setup work here, like attaching events
+or adding [upload hooks](#Upload-Hooks).
+
+```js
+export default class MyPlugin extends UIPlugin {
+	// ...
+	install() {
+		this.uppy.on('upload-progress', this.onProgress);
+		this.uppy.addPostProcessor(this.afterUpload);
+	}
+}
+```
+
+#### `uninstall()`
+
+Called when the plugin is removed, or the Uppy instance is closed. This should
+undo all the work done in the `install()` method.
+
+```js
+export default class MyPlugin extends UIPlugin {
+	// ...
+	uninstall() {
+		this.uppy.off('upload-progress', this.onProgress);
+		this.uppy.removePostProcessor(this.afterUpload);
+	}
+}
+```
+
+#### `afterUpdate()`
+
+Called after every state update with a debounce, after everything has mounted.
+
+#### `addTarget()`
+
+Use this to add your plugin to another plugin’s target. This is what
+`@uppy/dashboard` uses to add other plugins to its UI.
+
+### `UIPlugin`
+
+`UIPlugin` extends the `BasePlugin` class so it will also contain all the above
+methods.
+
+#### `mount(target)`
+
+Mount this plugin to the `target` element. `target` can be a CSS query selector,
+a DOM element, or another Plugin. If `target` is a Plugin, the source (current)
+plugin will register with the target plugin, and the latter can decide how and
+where to render the source plugin.
+
+This method can be overridden to support for different render engines.
+
+#### `render()`
+
+Render this plugin’s UI. Uppy uses [Preact](https://preactjs.com) as its view
+engine, so `render()` should return a Preact element. `render` is automatically
+called by Uppy on each state change.
+
+#### `onMount()`
+
+Called after Preact has rendered the components of the plugin.
+
+#### `update(state)`
+
+Called on each state update. You will rarely need to use this, unless if you
+want to build a UI plugin using something other than Preact.
+
+#### `onUnmount()`
+
+Called after the elements have been removed from the DOM. Can be used to do some
+clean up or other side-effects.
+
+## Upload Hooks
+
+When creating an upload, Uppy runs files through an upload pipeline. The
+pipeline consists of three parts, each of which can be hooked into:
+Preprocessing, Uploading, and Postprocessing. Preprocessors can be used to
+configure uploader plugins, encrypt files, resize images, etc., before uploading
+them. Uploaders do the actual uploading work, such as creating an XMLHttpRequest
+object and sending the file. Postprocessors do their work after files have been
+uploaded completely. This could be anything from waiting for a file to propagate
+across a CDN, to sending another request to relate some metadata to the file.
+
+Each hook is a function that receives an array containing the file IDs that are
+being uploaded, and returns a Promise to signal completion. Hooks are added and
+removed through `Uppy` methods:
+[`addPreProcessor`](/docs/uppy#addpreprocessorfn),
+[`addUploader`](/docs/uppy#adduploaderfn),
+[`addPostProcessor`](/docs/uppy#addpostprocessorfn), and their
+[`remove*`](/docs/uppy#removepreprocessorremoveuploaderremovepostprocessorfn)
+counterparts. Normally, hooks should be added during the plugin `install()`
+method, and removed during the `uninstall()` method.
+
+Additionally, upload hooks can fire events to signal progress.
+
+When adding hooks, make sure to bind the hook `fn` beforehand! Otherwise, it
+will be impossible to remove. For example:
+
+```js
+class MyPlugin extends BasePlugin {
+	constructor(uppy, opts) {
+		super(uppy, opts);
+		this.id = opts.id || 'MyPlugin';
+		this.type = 'example';
+		this.prepareUpload = this.prepareUpload.bind(this); // ← this!
+	}
+
+	prepareUpload(fileIDs) {
+		console.log(this); // `this` refers to the `MyPlugin` instance.
+		return Promise.resolve();
+	}
+
+	install() {
+		this.uppy.addPreProcessor(this.prepareUpload);
+	}
+
+	uninstall() {
+		this.uppy.removePreProcessor(this.prepareUpload);
+	}
+}
+```
+
+Or you can define the method as a class field:
+
+```js
+class MyPlugin extends UIPlugin {
+	constructor(uppy, opts) {
+		super(uppy, opts);
+		this.id = opts.id || 'MyPlugin';
+		this.type = 'example';
+	}
+
+	prepareUpload = (fileIDs) => {
+		// ← this!
+		console.log(this); // `this` refers to the `MyPlugin` instance.
+		return Promise.resolve();
+	};
+
+	install() {
+		this.uppy.addPreProcessor(this.prepareUpload);
+	}
+
+	uninstall() {
+		this.uppy.removePreProcessor(this.prepareUpload);
+	}
+}
+```
+
+## Progress events
+
+Progress events can be fired for individual files to show feedback to the user.
+For upload progress events, only emitting how many bytes are expected and how
+many have been uploaded is enough. Uppy will handle calculating progress
+percentages, upload speed, etc.
+
+Preprocessing and postprocessing progress events are plugin-dependent and can
+refer to anything, so Uppy doesn’t try to be smart about them. Processing
+progress events can be of two types: determinate or indeterminate. Some
+processing does not have meaningful progress beyond “not done” and “done”. For
+example, sending a request to initialize a server-side resource that will serve
+as the upload destination. In those situations, indeterminate progress is
+suitable. Other processing does have meaningful progress. For example,
+encrypting a large file. In those situations, determinate progress is suitable.
+
+Here are the relevant events:
+
+* [`preprocess-progress`](/docs/uppy#preprocess-progress)
+* [`upload-progress`](/docs/uppy#upload-progress)
+* [`postprocess-progress`](/docs/uppy#postprocess-progress)
+
+## JSX
+
+Since Uppy uses Preact and not React, the default Babel configuration for JSX
+elements does not work. You have to import the Preact `h` function and tell
+Babel to use it by adding a `/** @jsx h */` comment at the top of the file.
+
+See the Preact
+[Getting Started Guide](https://preactjs.com/guide/getting-started) for more on
+Babel and JSX.
+
+<!-- eslint-disable jsdoc/check-tag-names -->
+
+```jsx
+/** @jsx h */
+import { UIPlugin } from '@uppy/core';
+import { h } from 'preact';
+
+class NumFiles extends UIPlugin {
+	render() {
+		const numFiles = Object.keys(this.uppy.state.files).length;
+
+		return <div>Number of files: {numFiles}</div>;
+	}
+}
+```
+
+## Locales
+
+For any user facing language that you use while writing your Plugin, please
+provide them as strings in the `defaultLocale` property like so:
+
+```js
+this.defaultLocale = {
+	strings: {
+		youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
+		youCanOnlyUploadX: {
+			0: 'You can only upload %{smart_count} file',
+			1: 'You can only upload %{smart_count} files',
+			2: 'You can only upload %{smart_count} files',
+		},
+	},
+};
+```
+
+This allows them to be overridden by Locale Packs, or directly when users pass
+`locale: { strings: youCanOnlyUploadFileTypes: 'Something else completely about %{types}'} }`.
+For this to work, it’s also required that you call `this.i18nInit()` in the
+plugin constructor.
+
+## Example of a custom plugin
+
+Below is a full example of a
+[small plugin](https://github.com/arturi/uppy-plugin-image-compressor) that
+compresses images before uploading them. You can replace `compressorjs` method
+with any other work you need to do. This works especially well for async stuff,
+like calling an external API.
+
+<!-- eslint-disable consistent-return -->
+
+```js
+import { UIPlugin } from '@uppy/core';
+import Translator from '@uppy/utils/lib/Translator';
+import Compressor from 'compressorjs/dist/compressor.esm.js';
+
+class UppyImageCompressor extends UIPlugin {
+	constructor(uppy, opts) {
+		const defaultOptions = {
+			quality: 0.6,
+		};
+		super(uppy, { ...defaultOptions, ...opts });
+
+		this.id = this.opts.id || 'ImageCompressor';
+		this.type = 'modifier';
+
+		this.defaultLocale = {
+			strings: {
+				compressingImages: 'Compressing images...',
+			},
+		};
+
+		// we use those internally in `this.compress`, so they
+		// should not be overridden
+		delete this.opts.success;
+		delete this.opts.error;
+
+		this.i18nInit();
+	}
+
+	compress(blob) {
+		return new Promise(
+			(resolve, reject) =>
+				new Compressor(blob, {
+					...this.opts,
+					success(result) {
+						return resolve(result);
+					},
+					error(err) {
+						return reject(err);
+					},
+				}),
+		);
+	}
+
+	prepareUpload = (fileIDs) => {
+		const promises = fileIDs.map((fileID) => {
+			const file = this.uppy.getFile(fileID);
+			this.uppy.emit('preprocess-progress', file, {
+				mode: 'indeterminate',
+				message: this.i18n('compressingImages'),
+			});
+
+			if (!file.type.startsWith('image/')) {
+				return;
+			}
+
+			return this.compress(file.data)
+				.then((compressedBlob) => {
+					this.uppy.log(
+						`[Image Compressor] Image ${file.id} size before/after compression: ${file.data.size} / ${compressedBlob.size}`,
+					);
+					this.uppy.setFileState(fileID, { data: compressedBlob });
+				})
+				.catch((err) => {
+					this.uppy.log(
+						`[Image Compressor] Failed to compress ${file.id}:`,
+						'warning',
+					);
+					this.uppy.log(err, 'warning');
+				});
+		});
+
+		const emitPreprocessCompleteForAll = () => {
+			fileIDs.forEach((fileID) => {
+				const file = this.uppy.getFile(fileID);
+				this.uppy.emit('preprocess-complete', file);
+			});
+		};
+
+		// Why emit `preprocess-complete` for all files at once, instead of
+		// above when each is processed?
+		// Because it leads to StatusBar showing a weird “upload 6 files” button,
+		// while waiting for all the files to complete pre-processing.
+		return Promise.all(promises).then(emitPreprocessCompleteForAll);
+	};
+
+	install() {
+		this.uppy.addPreProcessor(this.prepareUpload);
+	}
+
+	uninstall() {
+		this.uppy.removePreProcessor(this.prepareUpload);
+	}
+}
+
+export default UppyImageCompressor;
+```

+ 11 - 0
docs/guides/building-your-own-ui-with-uppy.md

@@ -0,0 +1,11 @@
+---
+sidebar_position: 4
+---
+
+# Building your own UI with Uppy
+
+:::note
+
+This guide is in progress.
+
+:::

+ 93 - 0
docs/guides/choosing-uploader.md

@@ -0,0 +1,93 @@
+---
+sidebar_position: 1
+---
+
+# Choosing the uploader you need
+
+Versatile, reliable uploading is at the heart of Uppy. It has many configurable
+plugins to suit your needs. In this guide we will explain the different plugins,
+their strategies, and when to use them based on use cases.
+
+## Use cases
+
+### I want worry-free, plug-and-play uploads with Transloadit services
+
+Transloadit’s strength is versatility. By doing video, audio, images, documents,
+and more, you only need one vendor for [all your file processing
+needs][transloadit-services]. The [`@uppy/transloadit`][] plugin directly
+uploads to Transloadit so you only have to worry about creating a
+[template][transloadit-concepts]. It uses
+[Tus](#i-want-reliable-resumable-uploads) under the hood so you don’t have to
+sacrifice reliable, resumable uploads for convenience.
+
+You should use [`@uppy/transloadit`][] if you don’t want to host your own
+server, (optionally) need file processing, and store it in the service (such as
+S3 or GCS) of your liking. All with minimal effort.
+
+### I want reliable, resumable uploads
+
+[Tus][tus] is a new open protocol for resumable uploads built on HTTP. This
+means accidentally closing your tab or losing connection let’s you continue, for
+instance, your 10GB upload instead of starting all over.
+
+Tus supports any language, any platform, and any network. It requires a client
+and server integration to work. You can checkout the client and server
+[implementations][tus-implementations] to find the server in your preferred
+language. You can store files on the Tus server itself, but you can also use
+service integrations (such as S3) to store files externally.
+
+If you want reliable, resumable uploads: use [`@uppy/tus`][] to connect to your
+Tus server in a few lines of code.
+
+:::tip
+
+If you plan to let people upload _a lot_ of files, [`@uppy/tus`][] has
+exponential backoff built-in. Meaning if your server (or proxy) returns HTTP 429
+because it’s being overloaded, [`@uppy/tus`][] will find the ideal sweet spot to
+keep uploading without overloading.
+
+:::
+
+### I want to upload to AWS S3 (or S3-compatible storage) directly
+
+When you prefer a _client-to-storage_ over a _client-to-server-to-storage_ (such
+as [Transloadit](/docs/transloadit) or [Tus](/docs/tus)) setup. This may in some
+cases be preferable, for instance, to reduce costs or the complexity of running
+a server and load balancer with [Tus](/docs/tus).
+
+Uppy has two plugins to make this happen [`@uppy/aws-s3`][] and
+[`@uppy/aws-s3-multipart`][], but we are planning to merge the two plugins in
+the next major. You should use [`@uppy/aws-s3`][] with the new
+`shouldUseMultipart` option.
+
+:::info
+
+You can also save files in S3 with the [`/s3/store`][s3-robot] robot while still
+using the powers of Transloadit services.
+
+:::
+
+### I want to send regular HTTP uploads to my own server
+
+[`@uppy/xhr-upload`][] handles classic HTML multipart form uploads as well as
+uploads using the HTTP `PUT` method.
+
+[s3-robot]: https://transloadit.com/services/file-exporting/s3-store/
+
+[transloadit-services]: https://transloadit.com/services/
+
+[transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/
+
+[`@uppy/transloadit`]: /docs/transloadit
+
+[`@uppy/tus`]: /docs/tus
+
+[`@uppy/aws-s3-multipart`]: /docs/aws-s3-multipart
+
+[`@uppy/aws-s3`]: /docs/aws-s3-multipart
+
+[`@uppy/xhr-upload`]: /docs/xhr-upload
+
+[tus]: https://tus.io/
+
+[tus-implementations]: https://tus.io/implementations.html

+ 136 - 0
docs/guides/custom-stores.md

@@ -0,0 +1,136 @@
+# Custom stores
+
+If your app uses a state management library such as
+[Redux](https://redux.js.org), it can be useful to have Uppy store its state
+there instead—that way, you could write custom uploader UI components in the
+same way as the other components in the application.
+
+Uppy comes with two state management solutions (stores):
+
+* `@uppy/store-default`, a basic object-based store.
+* `@uppy/store-redux`, a store that uses a key in a Redux store.
+
+You can also use a third-party store:
+
+* [uppy-store-ngrx](https://github.com/rimlin/uppy-store-ngrx/), keeping Uppy
+  state in a key in an [Ngrx](https://github.com/ngrx/platform) store for use
+  with Angular.
+
+## Using stores
+
+To use a store, pass an instance to the
+[`store` option](/docs/uppy#store-defaultstore) in the Uppy constructor:
+
+```js
+import DefaultStore from '@uppy/store-default';
+
+const uppy = new Uppy({
+	store: new DefaultStore(),
+});
+```
+
+### `DefaultStore`
+
+Uppy uses the `DefaultStore` by default! You do not need to do anything to use
+it. It does not take any options.
+
+### `ReduxStore`
+
+The `ReduxStore` stores Uppy state on a key in an existing Redux store. The
+`ReduxStore` dispatches `uppy/STATE_UPDATE` actions to update state. When the
+state in Redux changes, it notifies Uppy. This way, you get most of the benefits
+of Redux, including support for the Redux Devtools and time traveling!
+
+Checkout our
+[Redux example](https://github.com/transloadit/uppy/tree/main/examples/redux)
+for a working demo.
+
+#### `opts.store`
+
+Pass a Redux store instance, from `Redux.createStore`. This instance should have
+the Uppy reducer mounted somewhere already.
+
+#### `opts.id`
+
+By default, the `ReduxStore` assumes Uppy state is stored on a `state.uppy[id]`
+key. `id` is randomly generated by the store constructor, but can be specified
+by passing an `id` option if it should be predictable.
+
+```js
+ReduxStore({
+	store,
+	id: 'avatarUpload',
+});
+```
+
+#### `opts.selector`
+
+If you’d rather not store the Uppy state under the `state.uppy` key at all, use
+the `selector` option to the `ReduxStore` constructor to tell it where to find
+state instead:
+
+```js
+const uppy = new Uppy({
+	store: ReduxStore({
+		store,
+		id: 'avatarUpload',
+		selector: (state) => state.pages.profile.uppy.avatarUpload,
+	}),
+});
+```
+
+Note that when specifying a custom selector, you **must** also specify a custom
+store ID. The store `id` tells the reducer in which property it should put
+Uppy’s state. The selector must then take the state from that property. In the
+example, we set the ID to `avatarUpload` and take the state from the
+`[reducer mount path].avatarUpload`.
+
+If your app uses [`reselect`](https://npmjs.com/package/reselect), its selectors
+work well with this!
+
+## Implementing Stores
+
+An Uppy store is an object with three methods.
+
+* `getState()` - Return the current state object.
+* `setState(patch)` - Merge the object `patch` into the current state.
+* `subscribe(listener)` - Call `listener` whenever the state changes. `listener`
+  is a function that should receive three parameters:
+  `(prevState, nextState, patch)`
+
+  The `subscribe()` method should return a function that “unsubscribes”
+  (removes) the `listener`.
+
+The default store implementation, for example, looks a bit like this:
+
+```js
+function createDefaultStore() {
+	let state = {};
+	const listeners = new Set();
+
+	return {
+		getState: () => state,
+		setState: (patch) => {
+			const prevState = state;
+			const nextState = { ...prevState, ...patch };
+
+			state = nextState;
+
+			listeners.forEach((listener) => {
+				listener(prevState, nextState, patch);
+			});
+		},
+		subscribe: (listener) => {
+			listeners.add(listener);
+			return () => listeners.remove(listener);
+		},
+	};
+}
+```
+
+A pattern like this, where users can pass options via a function call if
+necessary, is recommended.
+
+See the
+[@uppy/store-default](https://github.com/transloadit/uppy/tree/main/packages/%40uppy/store-default)
+package for more inspiration.

+ 651 - 0
docs/guides/migration-guides.md

@@ -0,0 +1,651 @@
+# Migration guides
+
+These cover all the major Uppy versions and how to migrate to them.
+
+## Migrate from Robodog to Uppy plugins
+
+Uppy is flexible and extensible through plugins. But the integration code could
+sometimes be daunting. This is what brought Robodog to life. An alternative with
+the same features, but with a more ergonomic and minimal API.
+
+But, it didn’t come with its own set of new problems:
+
+* It tries to do the exact same, but it looks like a different product.
+* It’s confusing for users whether they want to use Robodog or Uppy directly.
+* Robodog is more ergonomic because it’s limited. When you hit such a limit, you
+  need to refactor everything to Uppy with plugins.
+
+This has now led us to deprecating Robodog and embrace Uppy for its strong
+suits; modularity and flexibility. At the same time, we also introduced
+something to take away some repetitive integration code:
+[`@uppy/remote-sources`](/docs/remote-sources).
+
+To mimic the Robodog implementation with all its features, you can use the code
+snippet below. But chances are Robodog did more than you need so feel free to
+remove things or go through the [list of plugins](/docs/companion/) and install
+and use the ones you need.
+
+You can also checkout how we migrated the Robodog example ourselves in this
+[commit](https://github.com/transloadit/uppy/commit/089aaed615c77bafaf905e291b6b4e82aaeb2f6f).
+
+```js
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import RemoteSources from '@uppy/remote-sources';
+import Webcam from '@uppy/webcam';
+import ScreenCapture from '@uppy/screen-capture';
+import GoldenRetriever from '@uppy/golden-retriever';
+import ImageEditor from '@uppy/image-editor';
+import Audio from '@uppy/audio';
+import Transloadit, {
+	COMPANION_URL,
+	COMPANION_ALLOWED_HOSTS,
+} from '@uppy/transloadit';
+
+import '@uppy/core/dist/style.css';
+import '@uppy/dashboard/dist/style.css';
+import '@uppy/audio/dist/style.css';
+import '@uppy/screen-capture/dist/style.css';
+import '@uppy/image-editor/dist/style.css';
+
+new Uppy()
+	.use(Dashboard, {
+		inline: true,
+		target: '#app',
+		showProgressDetails: true,
+		proudlyDisplayPoweredByUppy: true,
+	})
+	.use(RemoteSources, {
+		companionUrl: COMPANION_URL,
+		companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
+	})
+	.use(Webcam, {
+		target: Dashboard,
+		showVideoSourceDropdown: true,
+		showRecordingLength: true,
+	})
+	.use(Audio, {
+		target: Dashboard,
+		showRecordingLength: true,
+	})
+	.use(ScreenCapture, { target: Dashboard })
+	.use(ImageEditor, { target: Dashboard })
+	.use(Transloadit, {
+		service: 'https://api2.transloadit.com',
+		async getAssemblyOptions(file) {
+			// This is where you configure your auth key, auth secret, and template ID
+			// /uppy/docs/transloadit/#getAssemblyOptions-file
+			//
+			// It is important to set the secret in production:
+			// https://transloadit.com/docs/topics/signature-authentication/
+			const response = await fetch('/some-endpoint');
+			return response.json();
+		},
+	});
+```
+
+## Migrate from Uppy 2.x to 3.x
+
+### Uppy is pure ESM
+
+Following the footsteps of many packages, we now only ship Uppy core and its
+plugins as
+[ECMAScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
+(ESM). On Uppy 2.x, we were shipping CommonJS.
+
+If are already using ESM yourself, or are using the CDN builds, nothing changes
+for you!
+
+If you are using CommonJS, you might need to add some tooling for everything to
+work, or you might want to refactor your codebase to ESM – refer to the
+[Pure ESM package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
+gist for added information and help on how to do that.
+
+### Robodog is deprecated
+
+See the [Robodog migration guide](#Migrate-from-Robodog-to-Uppy-plugins).
+
+### `@uppy/core`
+
+#### Remove `AggregateError` polyfill.
+
+It’s supported by most modern browsers and
+[can be polyfilled by the user](https://github.com/transloadit/uppy/pull/3532#discussion_r818602636)
+if needed.
+
+To migrate: install a `AggregateError` polyfill or use `core-js`.
+
+#### Remove `reset()` method.
+
+It’s a duplicate of `cancelAll`, but with a less intention revealing name.
+
+To migrate: use `cancelAll`.
+
+#### Remove backwards compatible exports (static properties on `Uppy`)\`
+
+`Uppy`, `UIPlugin`, `BasePlugin`, and `debugLogger` used to also be accessible
+on the `Uppy` export. This has now been removed due to the transition to ESM.
+
+To migrate: import the `Uppy` class by default and/or use named exports for
+everything else.
+
+#### `uppy.validateRestrictions()` now returns a `RestrictionError`
+
+This method used to return `{ result: false, reason: err.message }`, but that
+felt strange as it tries to mimic an error. Instead it now return a
+`RestrictionError`, which is extended `Error` class.
+
+To migrate: check the return value, if it’s defined you have an error, otherwise
+all went well. Note that the error is `return`’ed, it’s not `throw`’n, so you
+don’t have to `catch` it.
+
+### `@uppy/transloadit`
+
+Remove export of `ALLOWED_COMPANION_PATTERN`, `COMPANION`, and
+`COMPANION_PATTERN` in favor of `COMPANION_URL` and `COMPANION_ALLOWED_HOSTS`.
+This is to have more intention revealing names, `COMPANION` sounds like the
+Companion instance, `COMPANION_URL` makes it more clear that it’s a URL.
+
+These are properties can now be imported and used for remote sources plugins
+when using Transloadit:
+
+```js
+import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
+
+// ...
+uppy.use(Dropbox, {
+	companionUrl: COMPANION_URL,
+	companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
+});
+```
+
+### `@uppy/aws-s3-multipart`
+
+#### Make `headers` inside the return value of [`prepareUploadParts`](/docs/aws-s3-multipart/#prepareUploadParts-file-partData) part-indexed too.
+
+This is to allow custom headers to be set per part. See this
+[issue](https://github.com/transloadit/uppy/issues/3881) for details.
+
+To migrate: make headers part indexed like `presignedUrls`:
+`{ "headers": { "1": { "Content-MD5": "foo" } }}`.
+
+#### Remove `client` getter and setter.
+
+It’s internal usage only.
+
+To migrate: use exposed options only.
+
+### `@uppy/tus/`, `@uppy/aws-s3`, `@uppy/xhr-upload`
+
+Rename `metaFields` option to `allowedMetaFields`. Counter intuitively,
+`metaFields` is for _filtering_ which `metaFields` to send along with the
+request, not for adding extra meta fields to a request. As a lot of people were
+confused by this, and the name overlaps with the
+[`metaFields` option from Dashboard](/docs/dashboard/#metaFields), we renamed
+it.
+
+To migrate: use `allowedMetaFields`.
+
+### `@uppy/react`
+
+#### Uppy dependencies have become peer dependencies
+
+`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/file-input`, `@uppy/progress-bar`,
+and `@uppy/status-bar` are now peer dependencies. This means you don’t install
+all these packages if you only need one.
+
+To migrate: install only the packages you need. If you use the Dashboard
+component, you need `@uppy/dashboard`, and so onwards.
+
+#### Don’t expose `validProps` on the exported components.
+
+It’s internal usage only.
+
+To migrate: use exposed options only.
+
+### `@uppy/svelte`
+
+`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/progress-bar`, and
+`@uppy/status-bar` are now peer dependencies. This means you don’t install all
+these packages if you only need one.
+
+To migrate: install only the packages you need. If you use the Dashboard
+component, you need `@uppy/dashboard`, and so onwards.
+
+### `@uppy/vue`
+
+`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/file-input`, `@uppy/progress-bar`,
+and `@uppy/status-bar` are now peer dependencies. This means you don’t install
+all these packages if you only need one.
+
+To migrate: install only the packages you need. If you use the Dashboard
+component, you need `@uppy/dashboard`, and so onwards.
+
+### `@uppy/store-redux`
+
+Remove backwards compatible exports (static properties on `ReduxStore`).
+Exports, such as `reducer`, used to also be accessible on the `ReduxStore`
+export. This has now been removed due to the transition to ESM.
+
+To migrate: use named imports.
+
+### `@uppy/thumbnail-generator`
+
+Remove `rotateImage`, `protect`, and `canvasToBlob` from the plugin prototype.
+They are internal usage only.
+
+To migrate: use exposed options only.
+
+### Known issues
+
+* [`ERESOLVE could not resolve` on npm install](https://github.com/transloadit/uppy/issues/4057).
+* [@uppy/svelte reports a broken dependency with the Vite bundler](https://github.com/transloadit/uppy/issues/4069).
+
+## Migrate from Companion 3.x to 4.x
+
+### Minimum required Node.js version is v14.20.0
+
+Aligning with the Node.js
+[Long Term Support (LTS) schedule](https://nodejs.org/en/about/releases/) and to
+use modern syntax features.
+
+### `companion.app()` returns `{ app, emitter }` instead of `app`
+
+Companion 3.x provides the emitter as `companionEmitter` on `app`. As of 4.x, an
+object is returned with an `app` property (express middleware) and an `emitter`
+property (event emitter). This provides more flexibility in the future and
+follows best practices.
+
+### Removed `searchProviders` wrapper object inside `providerOptions`
+
+To use [`@uppy/unsplash`](/docs/unsplash), you had to configure Unsplash in
+Companion inside `providerOptions.searchProviders`. This is redundant, Unsplash
+is a provider as well so we removed the wrapper object.
+
+### Moved the `s3` options out of `providerOptions`
+
+To use AWS S3 for storage, you configured the `s3` object inside
+`providerOptions`. But as S3 is not a provider but a destination. To avoid
+confusion we moved the `s3` settings to the root settings object.
+
+### Removed compatibility for legacy Custom Provider implementations
+
+[Custom Provider](/docs/companion/#Adding-custom-providers) implementations must
+use the Promise API. The callback API is no longer supported.
+
+### Default to no ACL for AWS S3
+
+Default to no
+[ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html)
+for S3 uploads. Before the default was `public-read` but AWS now discourages
+ACLs. The environment variable `COMPANION_AWS_DISABLE_ACL` is also removed,
+instead Companion only uses `COMPANION_AWS_ACL`.
+
+### `protocol` sent from Uppy in any `get` request is now required (before it would default to Multipart).
+
+If you use any official Uppy plugins, then no migration is needed. For custom
+plugins that talk to Companion, make to send along the `protocol` header with a
+value of `multipart`, `s3Multipart`, or `tus`.
+
+### `emitSuccess` and `emitError` are now private methods on the `Uploader` class.
+
+It’s unlikely you’re using this, but it’s technically a breaking change. In
+general, don’t depend on implicitly internal methods, use exposed APIs instead.
+
+### Removed `chunkSize` backwards compatibility for AWS S3 Multipart
+
+`chunkSize` option will now be used as `partSize` in AWS multipart. Before only
+valid values would be respected. Invalid values would be ignored. Now any value
+will be passed on to the AWS SDK, possibly throwing an error on invalid values.
+
+### Removed backwards compatibility for `/metrics` endpoint
+
+The `metrics` option is a boolean flag to tell Companion whether to provide an
+endpoint `/metrics` with Prometheus metrics. Metrics will now always be served
+under `options.server.path`. Before v4.x, it would always be served under the
+root.
+
+For example: if `{ options: { metrics: true, server: { path: '/companion' }}}`,
+metrics will now be served under `/companion/metrics`. In v3.x, the metrics
+would be served under `/metrics`.
+
+## Migrate from Uppy 1.x to 2.x
+
+### New bundle requires manual polyfilling
+
+With 2.0, following in the footsteps of Microsoft, we are dropping support for
+IE11. As a result, we are able to remove all built-in polyfills, and the new
+bundle size is **25% smaller**! If you want your app to still support older
+browsers (such as IE11), you may need to add the following polyfills to your
+bundle:
+
+* [abortcontroller-polyfill](https://github.com/mo/abortcontroller-polyfill)
+* [core-js](https://github.com/zloirock/core-js)
+* [md-gum-polyfill](https://github.com/mozdevs/mediaDevices-getUserMedia-polyfill)
+* [resize-observer-polyfill](https://github.com/que-etc/resize-observer-polyfill)
+* [whatwg-fetch](https://github.com/github/fetch)
+
+If you’re using a bundler, you need import these before Uppy:
+
+```js
+import 'core-js';
+import 'whatwg-fetch';
+import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
+// Order matters here: AbortController needs fetch, which needs Promise (provided by core-js).
+
+import 'md-gum-polyfill';
+import ResizeObserver from 'resize-observer-polyfill';
+
+window.ResizeObserver ??= ResizeObserver;
+
+export { default } from '@uppy/core';
+export * from '@uppy/core';
+```
+
+If you’re using Uppy from a CDN, we now provide two bundles: one for up-to-date
+browsers that do not include polyfills and use modern syntax, and one for legacy
+browsers. When migrating, be mindful about the types of browsers you want to
+support:
+
+```html
+<!-- Modern browsers (recommended) -->
+<script src="https://releases.transloadit.com/uppy/v3.17.0/uppy.min.js"></script>
+
+<!-- Legacy browsers (IE11+) -->
+<script
+	nomodule
+	src="https://releases.transloadit.com/uppy/v3.17.0/uppy.legacy.min.js"
+></script>
+<script type="module">
+	import 'https://releases.transloadit.com/uppy/v3.17.0/uppy.min.js';
+</script>
+```
+
+Please note that while you may be able to get 2.0 to work in IE11 this way, we
+do not officially support it anymore.
+
+### Use `BasePlugin` or `UIPlugin` instead of `Plugin`
+
+[`@uppy/core`][core] used to provide a `Plugin` class for creating plugins. This
+was used for any official plugin, but also for users who want to create their
+own custom plugin. But, `Plugin` always came bundled with Preact, even if the
+plugin itself didn’t add any UI elements.
+
+`Plugin` has been replaced with `BasePlugin` and `UIPlugin`. `BasePlugin` is the
+minimum you need to create a plugin and `UIPlugin` adds Preact for rendering
+user interfaces.
+
+You can import them from [`@uppy/core`][core]:
+
+```js
+import { BasePlugin, UIPlugin } from '@uppy/core';
+```
+
+**Note:** some bundlers will include `UIPlugin` (and thus Preact) if you import
+from `@uppy/core`. To make sure this does not happen, you can import `Uppy` and
+`BasePlugin` directly:
+
+```js
+import Uppy from '@uppy/core/lib/Uppy.js';
+import BasePlugin from '@uppy/core/lib/BasePlugin.js';
+```
+
+### Use the latest Preact for your Uppy plugins
+
+Official plugins have already been upgraded. If you are using any custom
+plugins, upgrade Preact to the latest version. At the time of writing this is
+`10.5.13`.
+
+### Set plugin titles from locales
+
+Titles for plugins used to be set with the `title` property in the plugin
+options, but all other strings are set in `locale`. This has now been aligned.
+You should set your plugin title from the `locale` property.
+
+Before
+
+```js
+import Webcam from '@uppy/webcam';
+
+uppy.use(Webcam, {
+	title: 'Some title',
+});
+```
+
+After
+
+```js
+import Webcam from '@uppy/webcam';
+
+uppy.use(Webcam, {
+	locale: {
+		strings: {
+			title: 'Some title',
+		},
+	},
+});
+```
+
+### Initialize Uppy with the `new` keyword
+
+The default export `Uppy` is no longer callable as a function. This means you
+construct the `Uppy` instance using the `new` keyword.
+
+```js
+import Uppy from '@uppy/core';
+
+const uppy = new Uppy(); // correct.
+
+const otherUppy = Uppy(); // incorrect, will throw.
+```
+
+### Rename `allowMultipleUploads` to `allowMultipleUploadBatches`
+
+[`allowMultipleUploadBatches`](/docs/uppy/#allowmultipleuploadbatches) means
+allowing several calls to [`.upload()`](/docs/uppy/#upload), in other words, a
+user can add more files after already having uploaded some.
+
+<!--retext-simplify ignore multiple-->
+
+We have renamed this to be more intention revealing that this is about uploads,
+and not whether a user can choose multiple files for one upload.
+
+```js
+const uppy = new Uppy({
+	allowMultipleUploadBatches: true,
+});
+```
+
+### New default limits for [`@uppy/xhr-upload`][xhr] and [`@uppy/tus`][tus]
+
+The default limit has been changed from `0` to `5`. Setting this to `0` means no
+limit on concurrent uploads.
+
+You can change the limit on the Tus and XHR plugin options.
+
+```js
+uppy.use(Tus, {
+	// ...
+	limit: 10,
+});
+```
+
+```js
+uppy.use(XHRUpload, {
+	// ...
+	limit: 10,
+});
+```
+
+### TypeScript changes
+
+Uppy used to have loose types by default and strict types as an opt-in. The
+default export was a function that returned the `Uppy` class, and the types came
+bundled with the default export (`Uppy.SomeType`).
+
+```ts
+import Uppy from '@uppy/core';
+import Tus from '@uppy/tus';
+
+const uppy = Uppy<Uppy.StrictTypes>();
+
+uppy.use(Tus, {
+	invalidOption: null, // this will make the compilation fail!
+});
+```
+
+Uppy is now strictly typed by default and loose types have been removed.
+
+```ts
+// ...
+
+const uppy = new Uppy();
+
+uppy.use(Tus, {
+	invalidOption: null, // this will make the compilation fail!
+});
+```
+
+Uppy types are now individual exports and should be imported separately.
+
+<!-- eslint-disable @typescript-eslint/no-unused-vars -->
+
+```ts
+import type { PluginOptions, UIPlugin, PluginTarget } from '@uppy/core';
+```
+
+#### Event types
+
+[`@uppy/core`][core] provides an [`.on`](/docs/uppy/#uppy-on-39-event-39-action)
+method to listen to [events](/docs/uppy/#Events). The types for these events
+were loose and allowed for invalid events to be passed, such as
+`uppy.on('upload-errrOOOoooOOOOOrrrr')`.
+
+<!-- eslint-disable @typescript-eslint/no-unused-vars -->
+
+```ts
+// Before:
+
+type Meta = { myCustomMetadata: string };
+
+// Invalid event
+uppy.on<Meta>('upload-errrOOOoooOOOOOrrrr', () => {
+	// ...
+});
+
+// After:
+
+// Normal event signature
+uppy.on('complete', (result) => {
+	const successResults = result.successful;
+});
+
+// Custom signature
+type Meta = { myCustomMetadata: string };
+
+// Notice how the custom type has now become the second argument
+uppy.on<'complete', Meta>('complete', (result) => {
+	// The passed type is now merged into the `meta` types.
+	const meta = result.successful[0].meta.myCustomMetadata;
+});
+```
+
+Plugins that add their own events can merge with existing ones in `@uppy/core`
+with `declare module '@uppy/core' { ... }`. This is a TypeScript pattern called
+[module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation).
+For instance, when using [`@uppy/dashboard`][dashboard]:
+
+<!-- eslint-disable @typescript-eslint/no-unused-vars -->
+
+```ts
+uppy.on('dashboard:file-edit-start', (file) => {
+	const fileName = file.name;
+});
+```
+
+### Changes to pre-signing URLs for [`@uppy/aws-s3-multipart`][aws-s3-multipart]
+
+See the Uppy 2.0.0 announcement post about the batch
+[pre-signing URLs change](/blog/2021/08/2.0/#Batch-pre-signing-URLs-for-AWS-S3-Multipart).
+
+`prepareUploadPart` has been renamed to
+[`prepareUploadParts`](/docs/aws-s3-multipart/#prepareUploadParts-file-partData)
+(plural). See the documentation link on how to use this function.
+
+### Removed the `.run` method from [`@uppy/core`][core]
+
+The `.run` method on the `Uppy` instance has been removed. This method was
+already obsolete and only logged a warning. As of this major version, it no
+longer exists.
+
+### Removed `resume` and `removeFingerprintOnSuccess` options from [`@uppy/tus`][tus]
+
+Tus will now by default try to resume uploads if the upload has been started in
+the past.
+
+This also means tus will store some data in localStorage for each upload, which
+will automatically be removed on success. Making `removeFingerprintOnSuccess`
+obsolete too.
+
+### That’s it!
+
+Uppy 1.0 will continue to receive bug fixes for three more months (until <time datetime="2021-12-01">1 December 2021</time>), security fixes for one more
+year (until <time datetime="2022-09-01">1 September 2022</time>), but no more
+new features after today. Exceptions are unlikely, but _can_ be made – to
+accommodate those with commercial support contracts, for example.
+
+We hope you’ll waste no time in taking Uppy 2.0 out for a walk. When you do,
+please let us know what you thought of it on
+[Reddit](https://www.reddit.com/r/javascript/comments/penbr7/uppy_file_uploader_20_smaller_and_faster_modular/),
+[HN](https://news.ycombinator.com/item?id=28359287), ProductHunt, or
+[Twitter](https://twitter.com/uppy_io/status/1432399270846603264). We’re howling
+at the moon to hear from you!
+
+## Migrate from Companion 1.x to 2.x
+
+### Prerequisite
+
+Since v2, you now need to be running `node.js >= v10.20.1` to use Companion.
+
+### ProviderOptions
+
+In v2 the `google` and `microsoft` [providerOptions](/docs/companion/#Options)
+have been changed to `drive` and `onedrive` respectively.
+
+### OAuth Redirect URIs
+
+On your Providers’ respective developer platforms, the OAuth redirect URIs that
+you should supply has now changed from:
+
+`http(s)://$COMPANION_HOST_NAME/connect/$AUTH_PROVIDER/callback` in v1
+
+to:
+
+`http(s)://$COMPANION_HOST_NAME/$PROVIDER_NAME/redirect` in v2
+
+#### New Redirect URIs
+
+<div class="table-responsive">
+
+| Provider     | New Redirect URI                                  |
+| ------------ | ------------------------------------------------- |
+| Dropbox      | `https://$COMPANION_HOST_NAME/dropbox/redirect`   |
+| Google Drive | `https://$COMPANION_HOST_NAME/drive/redirect`     |
+| OneDrive     | `https://$COMPANION_HOST_NAME/onedrive/redirect`  |
+| Box          | `https://$YOUR_COMPANION_HOST_NAME/box/redirect`  |
+| Facebook     | `https://$COMPANION_HOST_NAME/facebook/redirect`  |
+| Instagram    | `https://$COMPANION_HOST_NAME/instagram/redirect` |
+
+</div>
+
+<!-- definitions -->
+
+[core]: /docs/uppy/
+
+[xhr]: /docs/xhr-upload/
+
+[dashboard]: /docs/dashboard/
+
+[aws-s3-multipart]: /docs/aws-s3-multipart/
+
+[tus]: /docs/tus/

+ 1562 - 0
docs/locales.mdx

@@ -0,0 +1,1562 @@
+---
+sidebar_position: 10
+---
+
+# Internationalisation
+
+Uppy speaks many languages, English being the default. You can use a locale pack
+to translate Uppy into your language of choice.
+
+:::tip
+
+Checkout
+[`@uppy/locales`](https://github.com/transloadit/uppy/tree/main/packages/%40uppy/locales)
+on GitHub to see the list of supported languages.
+
+:::
+
+## Using a locale pack from npm
+
+This is the recommended way. Install `@uppy/locales` package from npm, then
+choose the locale you’d like to use: `@uppy/locales/lib/LANGUAGE_CODE`.
+
+```bash
+npm i @uppy/core @uppy/locales
+```
+
+```js
+import Uppy from '@uppy/core';
+import German from '@uppy/locales/lib/de_DE';
+// see below for the full list of locales
+const uppy = new Uppy({
+	debug: true,
+	locale: German,
+});
+```
+
+## Using a locale pack from CDN
+
+Add a `<script>` tag with Uppy bundle and the locale pack you’d like to use. You
+can copy/paste the link from the CDN column in the locales table. The locale
+will attach itself to the `Uppy.locales` object.
+
+```html
+<script src="https://releases.transloadit.com/uppy/v3.17.0/uppy.min.js"></script>
+<script src="https://releases.transloadit.com/uppy/locales/v3.3.1/de_DE.min.js"></script>
+
+<script>
+	var uppy = new Uppy.Uppy({
+		debug: true,
+		locale: Uppy.locales.de_DE,
+	});
+</script>
+```
+
+## Overriding locale strings for a specific plugin
+
+Many plugins come with their own locale strings, and the packs we provide
+consist of most of those strings. You can, however, override a locale string for
+a specific plugin, regardless of whether you are using locale pack or not. See
+the plugin documentation for the list of locale strings it uses.
+
+```js
+import Uppy from '@uppy/core';
+import DragDrop from '@uppy/drag-drop';
+import Russian from '@uppy/locales/lib/ru_RU';
+
+const uppy = new Uppy({
+	debug: true,
+	autoProceed: true,
+	locale: Russian,
+});
+uppy.use(DragDrop, {
+	target: '.UppyDragDrop',
+	// We are using the ru_RU locale pack (set above in Uppy options),
+	// but you can also override specific strings like so:
+	locale: {
+		strings: {
+			browse: 'выберите ;-)',
+		},
+	},
+});
+```
+
+## List of locales
+
+<table>
+	<thead>
+		<tr>
+			<th>38 Locales</th>
+			<th>NPM</th>
+			<th>CDN</th>
+			<th>Source on GitHub</th>
+		</tr>
+	</thead>
+	<tbody>
+		<tr>
+			<td>
+				Arabic <small>Saudi Arabia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/ar_SA
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/ar_SA.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ar_SA.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/ar_SA.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ar_SA.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Bulgarian <small>Bulgaria</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/bg_BG
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/bg_BG.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>bg_BG.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/bg_BG.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>bg_BG.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Chinese <small>China</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/zh_CN
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/zh_CN.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>zh_CN.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/zh_CN.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>zh_CN.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Chinese <small>Taiwan</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/zh_TW
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/zh_TW.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>zh_TW.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/zh_TW.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>zh_TW.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Croatian <small>Croatia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/hr_HR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/hr_HR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hr_HR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/hr_HR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hr_HR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Czech <small>Czechia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/cs_CZ
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/cs_CZ.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>cs_CZ.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/cs_CZ.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>cs_CZ.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Danish <small>Denmark</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/da_DK
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/da_DK.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>da_DK.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/da_DK.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>da_DK.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Dutch <small>Netherlands</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/nl_NL
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/nl_NL.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>nl_NL.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/nl_NL.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>nl_NL.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				English <small>United States</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/en_US
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/en_US.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>en_US.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/en_US.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>en_US.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Finnish <small>Finland</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/fi_FI
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/fi_FI.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fi_FI.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/fi_FI.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fi_FI.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				French <small>France</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/fr_FR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/fr_FR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fr_FR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/fr_FR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fr_FR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Galician <small>Spain</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/gl_ES
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/gl_ES.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>gl_ES.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/gl_ES.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>gl_ES.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				German <small>Germany</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/de_DE
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/de_DE.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>de_DE.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/de_DE.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>de_DE.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Greek <small>Greece</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/el_GR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/el_GR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>el_GR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/el_GR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>el_GR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Hebrew <small>Israel</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/he_IL
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/he_IL.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>he_IL.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/he_IL.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>he_IL.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Hindi <small>India</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/hi_IN
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/hi_IN.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hi_IN.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/hi_IN.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hi_IN.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Hungarian <small>Hungary</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/hu_HU
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/hu_HU.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hu_HU.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/hu_HU.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>hu_HU.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Icelandic <small>Iceland</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/is_IS
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/is_IS.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>is_IS.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/is_IS.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>is_IS.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Indonesian <small>Indonesia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/id_ID
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/id_ID.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>id_ID.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/id_ID.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>id_ID.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Italian <small>Italy</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/it_IT
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/it_IT.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>it_IT.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/it_IT.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>it_IT.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Japanese <small>Japan</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/ja_JP
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/ja_JP.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ja_JP.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/ja_JP.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ja_JP.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Korean <small>South Korea</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/ko_KR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/ko_KR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ko_KR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/ko_KR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ko_KR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Norwegian Bokmål <small>Norway</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/nb_NO
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/nb_NO.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>nb_NO.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/nb_NO.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>nb_NO.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Persian <small>Iran</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/fa_IR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/fa_IR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fa_IR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/fa_IR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>fa_IR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Polish <small>Poland</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/pl_PL
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/pl_PL.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pl_PL.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/pl_PL.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pl_PL.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Portuguese <small>Brazil</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/pt_BR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/pt_BR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pt_BR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/pt_BR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pt_BR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Portuguese <small>Portugal</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/pt_PT
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/pt_PT.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pt_PT.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/pt_PT.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>pt_PT.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Romanian <small>Romania</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/ro_RO
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/ro_RO.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ro_RO.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/ro_RO.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ro_RO.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Russian <small>Russia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/ru_RU
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/ru_RU.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ru_RU.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/ru_RU.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>ru_RU.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Serbian <small>Serbia</small>
+				<small>(Cyrillic)</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/sr_RS_Cyrillic
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/sr_RS_Cyrillic.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sr_RS_Cyrillic.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/sr_RS_Cyrillic.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sr_RS_Cyrillic.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Serbian <small>Serbia</small>
+				<small>(Latin)</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/sr_RS_Latin
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/sr_RS_Latin.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sr_RS_Latin.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/sr_RS_Latin.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sr_RS_Latin.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Slovak <small>Slovakia</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/sk_SK
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/sk_SK.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sk_SK.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/sk_SK.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sk_SK.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Spanish <small>Spain</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/es_ES
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/es_ES.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>es_ES.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/es_ES.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>es_ES.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Spanish <small>Mexico</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/es_MX
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/es_MX.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>es_MX.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/es_MX.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>es_MX.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Swedish <small>Sweden</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/sv_SE
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/sv_SE.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sv_SE.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/sv_SE.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>sv_SE.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Thai <small>Thailand</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/th_TH
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/th_TH.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>th_TH.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/th_TH.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>th_TH.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Turkish <small>Turkey</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/tr_TR
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/tr_TR.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>tr_TR.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/tr_TR.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>tr_TR.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Ukrainian <small>Ukraine</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/uk_UA
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/uk_UA.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>uk_UA.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/uk_UA.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>uk_UA.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Uzbek <small>Uzbekistan</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/uz_UZ
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/uz_UZ.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>uz_UZ.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/uz_UZ.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>uz_UZ.js</code>
+				</a>
+			</td>
+		</tr>
+		<tr>
+			<td>
+				Vietnamese <small>Vietnam</small>
+			</td>
+			<td>
+				<code class="raw">
+					<a
+						href="https://www.npmjs.com/package/@uppy/locales"
+						target="_blank"
+						rel="noopener"
+					>
+						@uppy/locales
+					</a>
+					/lib/vi_VN
+				</code>
+			</td>
+			<td>
+				<a
+					href="https://releases.transloadit.com/uppy/locales/v3.3.1/vi_VN.min.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>vi_VN.min.js</code>
+				</a>
+			</td>
+			<td>
+				✏️{' '}
+				<a
+					href="https://github.com/transloadit/uppy/blob/main/packages/%40uppy/locales/src/vi_VN.js"
+					target="_blank"
+					rel="noopener"
+				>
+					<code>vi_VN.js</code>
+				</a>
+			</td>
+		</tr>
+	</tbody>
+</table>
+
+## Contributing a new language
+
+If you speak a language we don’t yet support, you can contribute! Here’s how you
+do it:
+
+1. Go to the
+   [uppy/locales](https://github.com/transloadit/uppy/tree/main/packages/%40uppy/locales/src)
+   directory in the Uppy GitHub repo.
+2. Go to `en_US.js` and copy its contents, as English is the most up-to-date
+   locale.
+3. Press “Create new file”, name it according to the
+   [`language_COUNTRY` format](http://www.i18nguy.com/unicode/language-identifiers.html),
+   make sure to use underscore `_` as a divider. Examples: `en_US`, `en_GB`,
+   `ru_RU`, `ar_AE`. Variants should be trailing, for example `sr_RS_Latin` for
+   Serbian Latin vs Cyrillic.
+4. If your language has different pluralization rules than English, update the
+   `pluralize` implementation. If you are unsure how to do this, please ask us
+   for help in a [GitHub issue](https://github.com/transloadit/uppy/issues/new).
+5. Paste what you’ve copied from `en_US.js` and use it as a starting point to
+   translate strings into your language.
+6. When you are ready, save the file — this should create a PR that we’ll then
+   review 🎉 Thanks!

+ 4 - 0
docs/presets/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "Presets",
+	"position": 9
+}

+ 141 - 0
docs/presets/remote-sources.mdx

@@ -0,0 +1,141 @@
+---
+sidebar_position: 1
+slug: /remote-sources
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Remote sources
+
+`@uppy/remote-sources` is a preset plugin (meaning it bundles and sets up other
+plugins) to add all the available remote sources, such Instagram, Google Drive,
+Dropbox, and others to Uppy Dashboard in one package.
+
+:::note
+
+Remote Sources requires Dashboard and automatically installs all its plugins to
+it.
+
+:::
+
+## When should I use it?
+
+`@uppy/remote-sources` aims to simplify the setup for adding Companion plugins,
+when you want to share the configuration between plugins. If you want your users
+to upload files from any of the remote sources that Uppy offers, this plugin is
+recommended.
+
+A [Companion](/docs/companion) instance is required for the Remote Sources
+plugin to work. Companion handles authentication with the remote services (such
+as Facebook, Dropbox, etc.), downloads the files, and uploads them to the
+destination. This saves the user bandwidth, especially helpful if they are on a
+mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/remote-sources
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/remote-sources
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { RemoteSources } from "{{UPPY_JS_URL}}"
+        const RemoteSources = new Uppy().use(RemoteSources)
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import RemoteSources from '@uppy/remote-sources';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy();
+  .use(Dashboard);
+  .use(RemoteSources, { companionUrl: 'https://your-companion-url' });
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `RemoteSources`).
+
+#### `sources`
+
+List of remote sources that will be enabled (`array`, default:
+`['Box', 'Dropbox', 'Facebook', 'GoogleDrive','Instagram', 'OneDrive', 'Unsplash', 'Url', 'Zoom']`).
+
+You don’t need to specify them manually or change them, but if you want to alter
+the order in which they appear in the Dashboard, or disable some sources, this
+option is for you.
+
+```js
+uppy.use(RemoteSources, {
+	companionUrl: 'https://your-companion-url',
+	sources: ['Instagram', 'GoogleDrive', 'Unsplash', 'Url'],
+});
+```
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorized URL(s)/URL pattern(s) from which OAuth responses should
+be accepted (`string | RegExp | Array<string | RegExp>`, Default:
+`companionUrl`).
+
+This value can be a `string`, a `RegExp` object, or an array of both.
+
+This is useful when you have your [Companion](/docs/companion) running on
+several hosts. Otherwise, the default value, which is `companionUrl`, should do
+fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the [`Request.credentials` value][], which tells the
+plugin whether to send cookies to [Companion](/docs/companion) (`string`,
+default: `same-origin`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string`, `Element`, `Function`, or `UIPlugin`, default: `Dashboard`).
+
+[`request.credentials` value]:
+	https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

+ 58 - 0
docs/quick-start.mdx

@@ -0,0 +1,58 @@
+---
+sidebar_position: 1
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+import { QuickStartLinks } from '../src/components/QuickStartLinks/QuickStartLinks.tsx';
+
+# Quick start
+
+Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly
+with any application. It’s fast, has a comprehensible API and lets you worry
+about more important problems than building a file uploader.
+
+:::tip
+
+You can take Uppy for a walk inside CodeSandbox with a
+[minimal drag & drop](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js)
+experience or a
+[full featured dashboard](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+<QuickStartLinks
+	items={[
+		{
+			name: 'I want a full featured, extendable UI',
+			description: 'Learn more about the Dashboard',
+			link: '/docs/dashboard',
+		},
+		{
+			name: 'I want a minimal drag & drop UI',
+			description: 'We have a lightweight plugin for that',
+			link: '/docs/drag-drop',
+		},
+		{
+			name: 'Which uploader do I need?',
+			description: 'Choosing the uploader you need',
+			link: '/docs/guides/choosing-uploader',
+		},
+		{
+			name: 'I want to add files from remote sources',
+			description: 'Such as Google Drive, Dropbox, Instagram',
+			link: '/docs/companion',
+		},
+		{
+			name: 'I’d like a project example',
+			description: 'Try out one of our extensive example projects',
+			link: 'https://github.com/transloadit/uppy/tree/main/examples',
+		},
+		{
+			name: 'I have a question',
+			description: 'Our community forum is there to help',
+			link: 'https://community.transloadit.com/',
+		},
+	]}
+/>

+ 4 - 0
docs/sources/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "Sources",
+	"position": 6
+}

+ 134 - 0
docs/sources/audio.mdx

@@ -0,0 +1,134 @@
+---
+sidebar_position: 3
+slug: /audio
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Audio
+
+The `@uppy/audio` plugin lets you record audio using a built-in or external
+microphone, or any other audio device, on desktop and mobile. The UI shows real
+time sound wavelength when recording.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want users to record audio on their computer.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/audio
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/audio
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dashboard, Audio } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dashboard, { inline: true, target: 'body' })
+        uppy.use(Audio, { target: Uppy.Dashboard })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {3,7,11} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Audio from '@uppy/audio';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+import '@uppy/audio/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(Audio, { target: Dashboard });
+```
+
+### API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'audio'`).
+
+#### `title`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs
+(`string`, default: `'Audio'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the audio into (`string` or
+`Element`, default: `null`).
+
+#### `showAudioSourceDropdown`
+
+Configures whether to show a dropdown to select audio device (`boolean`,
+default: `false`).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameAudio: 'Audio',
+		// Used as the label for the button that starts an audio recording.
+		// This is not visibly rendered but is picked up by screen readers.
+		startAudioRecording: 'Begin audio recording',
+		// Used as the label for the button that stops an audio recording.
+		// This is not visibly rendered but is picked up by screen readers.
+		stopAudioRecording: 'Stop audio recording',
+		// Title on the “allow access” screen
+		allowAudioAccessTitle: 'Please allow access to your microphone',
+		// Description on the “allow access” screen
+		allowAudioAccessDescription:
+			'In order to record audio, please allow microphone access for this site.',
+		// Title on the “device not available” screen
+		noAudioTitle: 'Microphone Not Available',
+		// Description on the “device not available” screen
+		noAudioDescription:
+			'In order to record audio, please connect a microphone or another audio input device',
+		// Message about file size will be shown in an Informer bubble
+		recordingStoppedMaxSize:
+			'Recording stopped because the file size is about to exceed the limit',
+		// Used as the label for the counter that shows recording length (`1:25`).
+		// This is not visibly rendered but is picked up by screen readers.
+		recordingLength: 'Recording length %{recording_length}',
+		// Used as the label for the submit checkmark button.
+		// This is not visibly rendered but is picked up by screen readers.
+		submitRecordedFile: 'Submit recorded file',
+		// Used as the label for the discard cross button.
+		// This is not visibly rendered but is picked up by screen readers.
+		discardRecordedFile: 'Discard recorded file',
+	},
+};
+```

+ 5 - 0
docs/sources/companion-plugins/_category_.json

@@ -0,0 +1,5 @@
+{
+	"label": "Companion plugins",
+	"position": 3,
+	"collapsed": false
+}

+ 189 - 0
docs/sources/companion-plugins/box.mdx

@@ -0,0 +1,189 @@
+---
+slug: /box
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Box
+
+The `@uppy/box` plugin lets users import files from their
+[Box](https://www.box.com/en-nl/home) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Box](https://www.box.com/en-nl/home) account.
+
+A [Companion](/docs/companion) instance is required for the Box plugin to work.
+Companion handles authentication with Box, downloads the files, and uploads them
+to the destination. This saves the user bandwidth, especially helpful if they
+are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/box
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/box
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Box } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Box, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Box requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Box from '@uppy/box';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy().use(Dashboard, { inline: true, target: '#dashboard' }).use(Box, {
+	target: Dashboard,
+	companionUrl: 'https://your-companion.com',
+});
+```
+
+### Use in Companion
+
+You can create a Box App on the
+[Box Developers site](https://app.box.com/developers/console).
+
+Things to note:
+
+- Choose `Custom App` and select the `Standard OAuth 2.0 (User Authentication)`
+  app type.
+- You must enable full write access, or you will get
+  [403 when downloading files](https://support.box.com/hc/en-us/community/posts/360049195613-403-error-while-file-download-API-Call)
+
+You’ll be redirected to the app page. This page lists the client ID (app key)
+and client secret (app secret), which you should use to configure Companion.
+
+The app page has a `"Redirect URIs"` field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/box/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/box/redirect
+```
+
+You can only use the integration with your own account initially. Make sure to
+apply for production status on the app page before you publish your app, or your
+users will not be able to sign in!
+
+Configure the Box key and secret. With the standalone Companion server, specify
+environment variables:
+
+```shell
+export COMPANION_BOX_KEY="Box API key"
+export COMPANION_BOX_SECRET="Box API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		box: {
+			key: 'Box API key',
+			secret: 'Box API secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Box'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Box'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameBox: 'Box',
+	},
+};
+```

+ 188 - 0
docs/sources/companion-plugins/dropbox.mdx

@@ -0,0 +1,188 @@
+---
+slug: /dropbox
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Dropbox
+
+The `@uppy/dropbox` plugin lets users import files from their
+[Dropbox](https://www.dropbox.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Dropbox](https://www.dropbox.com) account.
+
+A [Companion](/docs/companion) instance is required for the Dropbox plugin to
+work. Companion handles authentication with Dropbox, downloads the files, and
+uploads them to the destination. This saves the user bandwidth, especially
+helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/dropbox
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/dropbox
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dropbox } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dropbox, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Dropbox requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Dropbox from '@uppy/dropbox';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy().use(Dashboard, { inline: true, target: '#dashboard' }).use(Dropbox, {
+	target: Dashboard,
+	companionUrl: 'https://your-companion.com',
+});
+```
+
+### Use in Companion
+
+You can create a Dropbox App on the
+[Dropbox Developers site](https://www.dropbox.com/developers/apps/create).
+
+Things to note:
+
+- Choose the “Dropbox API”, not the business variant.
+- Typically you’ll want “Full Dropbox” access, unless you are absolutely certain
+  that you need the other one.
+
+You’ll be redirected to the app page. This page lists the app key and app
+secret, which you should use to configure Companion as shown above.
+
+The app page has a “Redirect URIs” field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/dropbox/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/dropbox/redirect
+```
+
+You can only use the integration with your own account initially. Make sure to
+apply for production status on the app page before you publish your app, or your
+users will not be able to sign in!
+
+Configure the Dropbox key and secret. With the standalone Companion server,
+specify environment variables:
+
+```shell
+export COMPANION_DROPBOX_KEY="Dropbox API key"
+export COMPANION_DROPBOX_SECRET="Dropbox API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		dropbox: {
+			key: 'Dropbox API key',
+			secret: 'Dropbox API secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Dropbox'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Dropbox'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameDropbox: 'Dropbox',
+	},
+};
+```

+ 187 - 0
docs/sources/companion-plugins/facebook.mdx

@@ -0,0 +1,187 @@
+---
+slug: /facebook
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Facebook
+
+The `@uppy/facebook` plugin lets users import files from their
+[Facebook](https://www.facebook.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Facebook](https://www.facebook.com) account.
+
+A [Companion](/docs/companion) instance is required for the Facebook plugin to
+work. Companion handles authentication with Facebook, downloads the files, and
+uploads them to the destination. This saves the user bandwidth, especially
+helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/facebook
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/facebook
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Facebook } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Facebook, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Facebook requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Facebook from '@uppy/facebook';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(Facebook, {
+		target: Dashboard,
+		companionUrl: 'https://your-companion.com',
+	});
+```
+
+### Use in Companion
+
+You can create a Facebook App on the
+[Facebook Developers site](https://developers.facebook.com/).
+
+The app page has a “Redirect URIs” field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/facebook/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/facebook/redirect
+```
+
+You can only use the integration with your own account initially. Make sure to
+apply for production status on the app page before you publish your app, or your
+users will not be able to sign in!
+
+You need to set up OAuth in your Facebook app for Companion to be able to
+connect to users’ Facebook accounts. You have to enable “Advanced Access” for
+the `user_photos` permission. A precondition of that is “Business Verification”
+which involves setting up a Meta Business Account and submitting documents to
+prove business ownership.
+
+Configure the Facebook key and secret. With the standalone Companion server,
+specify environment variables:
+
+```shell
+export COMPANION_FACEBOOK_KEY="Facebook API key"
+export COMPANION_FACEBOOK_SECRET="Facebook API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		facebook: {
+			key: 'Facebook API key',
+			secret: 'Facebook API secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Facebook'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Facebook'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameFacebook: 'Facebook',
+	},
+};
+```

+ 191 - 0
docs/sources/companion-plugins/google-drive.mdx

@@ -0,0 +1,191 @@
+---
+slug: /google-drive
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Google Drive
+
+The `@uppy/google-drive` plugin lets users import files from their
+[Google Drive](https://www.drive.google.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Google Drive](https://www.drive.google.com) account.
+
+A [Companion](/docs/companion) instance is required for the Google Drive plugin
+to work. Companion handles authentication with Google Drive, downloads the
+files, and uploads them to the destination. This saves the user bandwidth,
+especially helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/google-drive
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/google-drive
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, GoogleDrive } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(GoogleDrive, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Google Drive requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import GoogleDrive from '@uppy/google-drive';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(GoogleDrive, {
+		target: Dashboard,
+		companionUrl: 'https://your-companion.com',
+	});
+```
+
+### Use in Companion
+
+To sign up for API keys, go to the
+[Google Developer Console](https://console.developers.google.com/).
+
+Create a project for your app if you don’t have one yet.
+
+- On the project’s dashboard,
+  [enable the Google Drive API](https://developers.google.com/drive/api/v3/enable-drive-api).
+- [Set up OAuth authorization](https://developers.google.com/drive/api/v3/about-auth).
+  - Under scopes, add the `https://www.googleapis.com/auth/drive.readonly` Drive
+    API scope.
+  - Due to this being a sensitive scope, your app must complete Google’s OAuth
+    app verification before being granted access. See
+    [OAuth App Verification Help Center](https://support.google.com/cloud/answer/13463073)
+    for more information.
+
+The app page has a `"Redirect URIs"` field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/drive/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/drive/redirect
+```
+
+Google will give you an OAuth client ID and client secret.
+
+Configure the Google Drive key and secret in Companion. With the standalone
+Companion server, specify environment variables:
+
+```shell
+export COMPANION_GOOGLE_KEY="Google Drive OAuth client ID"
+export COMPANION_GOOGLE_SECRET="Google Drive OAuth client secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		drive: {
+			key: 'Google Drive OAuth client ID',
+			secret: 'Google Drive OAuth client secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'GoogleDrive'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'GoogleDrive'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameGoogleDrive: 'GoogleDrive',
+	},
+};
+```

+ 181 - 0
docs/sources/companion-plugins/instagram.mdx

@@ -0,0 +1,181 @@
+---
+slug: /instagram
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Instagram
+
+The `@uppy/instagram` plugin lets users import files from their
+[Instagram](https://instagram.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Instagram](https://instagram.com) account.
+
+A [Companion](/docs/companion) instance is required for the Instagram plugin to
+work. Companion handles authentication with Instagram, downloads the files, and
+uploads them to the destination. This saves the user bandwidth, especially
+helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/instagram
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/instagram
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Instagram } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Instagram, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Instagram requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Instagram from '@uppy/instagram';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(Instagram, {
+		target: Dashboard,
+		companionUrl: 'https://your-companion.com',
+	});
+```
+
+### Use in Companion
+
+To sign up for API keys, go to the
+[Instagram Platform from Meta](https://developers.facebook.com/products/instagram/).
+
+Create a project for your app if you don’t have one yet.
+
+The app page has a `"Redirect URIs"` field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/instagram/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/instagram/redirect
+```
+
+Meta will give you an OAuth client ID and client secret.
+
+Configure the Instagram key and secret in Companion. With the standalone
+Companion server, specify environment variables:
+
+```shell
+export COMPANION_INSTAGRAM_KEY="Instagram OAuth client ID"
+export COMPANION_INSTAGRAM_SECRET="Instagram OAuth client secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		drive: {
+			key: 'Instagram OAuth client ID',
+			secret: 'Instagram OAuth client secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Instagram'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Instagram'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameInstagram: 'Instagram',
+	},
+};
+```

+ 181 - 0
docs/sources/companion-plugins/onedrive.mdx

@@ -0,0 +1,181 @@
+---
+slug: /onedrive
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# OneDrive
+
+The `@uppy/onedrive` plugin lets users import files from their
+[OneDrive](https://onedrive.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[OneDrive](https://onedrive.com) account.
+
+A [Companion](/docs/companion) instance is required for the OneDrive plugin to
+work. Companion handles authentication with OneDrive, downloads the files, and
+uploads them to the destination. This saves the user bandwidth, especially
+helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/onedrive
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/onedrive
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, OneDrive } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(OneDrive, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using OneDrive requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import OneDrive from '@uppy/onedrive';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(OneDrive, {
+		target: Dashboard,
+		companionUrl: 'https://your-companion.com',
+	});
+```
+
+### Use in Companion
+
+To sign up for API keys, go to the
+[Azure Platform from Microsoft](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade).
+
+Create a project for your app if you don’t have one yet.
+
+The app page has a `"Redirect URIs"` field. Here, add:
+
+```
+https://$YOUR_COMPANION_HOST_NAME/onedrive/redirect
+```
+
+If you are using Transloadit hosted Companion:
+
+```
+https://api2.transloadit.com/companion/onedrive/redirect
+```
+
+Microsoft will give you an OAuth client ID and client secret.
+
+Configure the OneDrive key and secret in Companion. With the standalone
+Companion server, specify environment variables:
+
+```shell
+export COMPANION_ONEDRIVE_KEY="OneDrive Application ID"
+export COMPANION_ONEDRIVE_SECRET="OneDrive OAuth client secret value"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		onedrive: {
+			key: 'OneDrive Application ID',
+			secret: 'OneDrive OAuth client secret value',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'OneDrive'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'OneDrive'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameOneDrive: 'OneDrive',
+	},
+};
+```

+ 166 - 0
docs/sources/companion-plugins/unsplash.mdx

@@ -0,0 +1,166 @@
+---
+slug: /unsplash
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Unsplash
+
+The `@uppy/unsplash` plugin lets users import files from their
+[Unsplash](https://unsplash.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their
+[Unsplash](https://unsplash.com) account.
+
+A [Companion](/docs/companion) instance is required for the Unsplash plugin to
+work. Companion handles authentication with Unsplash, downloads the files, and
+uploads them to the destination. This saves the user bandwidth, especially
+helpful if they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/unsplash
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/unsplash
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Unsplash } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Unsplash, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Unsplash requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Unsplash from '@uppy/unsplash';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(Unsplash, {
+		target: Dashboard,
+		companionUrl: 'https://your-companion.com',
+	});
+```
+
+### Use in Companion
+
+You can create a Unsplash App on the
+[Unsplash Developers site](https://unsplash.com/developers). You’ll be
+redirected to the app page, this page lists the app key and app secret.
+
+Configure the Unsplash key and secret. With the standalone Companion server,
+specify environment variables:
+
+```shell
+export COMPANION_UNSPLASH_KEY="Unsplash API key"
+export COMPANION_UNSPLASH_SECRET="Unsplash API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		unsplash: {
+			key: 'Unsplash API key',
+			secret: 'Unsplash API secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Unsplash'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Unsplash'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameUnsplash: 'Unsplash',
+	},
+};
+```

+ 149 - 0
docs/sources/companion-plugins/url.mdx

@@ -0,0 +1,149 @@
+---
+slug: /url
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Import from URL
+
+The `@uppy/url` plugin allows users to import files from the internet. Paste any
+URL and it will be added!
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files any URL.
+
+A [Companion](/docs/companion) instance is required for the URL plugin to work.
+This saves the user bandwidth, especially helpful if they are on a mobile
+connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+:::note
+
+Companion has
+[Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
+(SSRF) protections built-in so you don’t have to worry about the security
+implications of arbitrary URLs.
+
+:::
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/url
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/url
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Url } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Url, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using `@uppy/url` only requires setup in Uppy.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Url from '@uppy/url';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy().use(Dashboard, { inline: true, target: '#dashboard' }).use(Url, {
+	target: Dashboard,
+	companionUrl: 'https://your-companion.com',
+});
+```
+
+### Use in Companion
+
+Companion supports this plugin out-of-the-box so integration is required on this
+side.
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Url'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Url'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameUrl: 'Url',
+	},
+};
+```

+ 160 - 0
docs/sources/companion-plugins/zoom.mdx

@@ -0,0 +1,160 @@
+---
+slug: /zoom
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Zoom
+
+The `@uppy/zoom` plugin lets users import files from their
+[Zoom](https://zoom.com) account.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want to let users import files from their [Zoom](https://zoom.com)
+account.
+
+A [Companion](/docs/companion) instance is required for the Zoom plugin to work.
+Companion handles authentication with Zoom, downloads the files, and uploads
+them to the destination. This saves the user bandwidth, especially helpful if
+they are on a mobile connection.
+
+You can self-host Companion or get a hosted version with any
+[Transloadit plan](https://transloadit.com/pricing/).
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/zoom
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/zoom
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Zoom } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Zoom, {
+          // Options
+        })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+Using Zoom requires setup in both Uppy and Companion.
+
+### Use in Uppy
+
+```js {10-13} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Zoom from '@uppy/zoom';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy().use(Dashboard, { inline: true, target: '#dashboard' }).use(Zoom, {
+	target: Dashboard,
+	companionUrl: 'https://your-companion.com',
+});
+```
+
+### Use in Companion
+
+Configure the Zoom key and secret. With the standalone Companion server, specify
+environment variables:
+
+```shell
+export COMPANION_ZOOM_KEY="Zoom API key"
+export COMPANION_ZOOM_SECRET="Zoom API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+
+```js
+companion.app({
+	providerOptions: {
+		zoom: {
+			key: 'Zoom API key',
+			secret: 'Zoom API secret',
+		},
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Zoom'`).
+
+#### `title`
+
+Title / name shown in the UI, such as Dashboard tabs (`string`, default:
+`'Zoom'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionAllowedHosts`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted
+(`string` or `RegExp` or `Array`, default: `companionUrl`).
+
+This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
+useful when you have your [Companion](/docs/companion) running on several hosts.
+Otherwise, the default value should do fine.
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameZoom: 'Zoom',
+	},
+};
+```

+ 166 - 0
docs/sources/file-input.mdx

@@ -0,0 +1,166 @@
+---
+sidebar_position: 3
+slug: /file-input
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# File input
+
+The `@uppy/file-input` plugin is the most barebones UI for selecting files — it
+shows a single button that, when clicked, opens up the browser’s file selector.
+
+## When should I use it?
+
+When you want users to select files from their local machine with a minimal UI.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/file-input
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/file-input
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, FileInput } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(FileInput, { target: document.body })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js showLineNumbers
+import Uppy from '@uppy/core';
+import FileInput from '@uppy/file-input';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/file-input/dist/style.css';
+
+new Uppy().use(FileInput, { target: '#uppy-file-input' });
+```
+
+:::note
+
+The `@uppy/file-input` plugin includes some basic styles for use with the
+[`pretty`](#pretty-true) option, like shown in the
+[example](/examples/xhrupload). You can also choose not to use it and provide
+your own styles instead.
+
+Import general Core styles from `@uppy/core/dist/style.css` first, then add the
+File Input styles from `@uppy/file-input/dist/style.css`. A minified version is
+also available as `style.min.css` at the same path. The way to do import depends
+on your build system.
+
+:::
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'FileInput'`).
+
+#### `title`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs
+(`string`, default: `'File Input'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the audio into (`string` or
+`Element`, default: `null`).
+
+#### `pretty`
+
+When true, display a styled button that, when clicked, opens the file selector
+UI. When false, a plain old browser `<input type="file">` element is shown
+(`boolean`, default: `true`).
+
+#### `inputName`
+
+The `name` attribute for the `<input type="file">` element (`string`, default:
+`'files[]'`).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		// The same key is used for the same purpose by @uppy/robodog's `form()` API, but our
+		// locale pack scripts can't access it in Robodog. If it is updated here, it should
+		// also be updated there!
+		chooseFiles: 'Choose files',
+	},
+};
+```
+
+## Custom file inpt
+
+If you don’t like the look/feel of the button rendered by `@uppy/file-input`,
+feel free to forgo the plugin and use your own custom button on a page, like so:
+
+```html
+<input type="file" id="my-file-input" />
+```
+
+Then add this JS to attach it to Uppy:
+
+```js
+const uppy = new Uppy(/* ... */);
+const fileInput = document.querySelector('#my-file-input');
+
+fileInput.addEventListener('change', (event) => {
+	const files = Array.from(event.target.files);
+
+	files.forEach((file) => {
+		try {
+			uppy.addFile({
+				source: 'file input',
+				name: file.name,
+				type: file.type,
+				data: file,
+			});
+		} catch (err) {
+			if (err.isRestriction) {
+				// handle restrictions
+				console.log('Restriction error:', err);
+			} else {
+				// handle other errors
+				console.error(err);
+			}
+		}
+	});
+});
+
+// Clear the `<input>` after the upload or when the file was removed
+// so the file can be uploaded again (see
+// https://github.com/transloadit/uppy/issues/2640#issuecomment-731034781).
+uppy.on('file-removed', () => {
+	fileInput.value = null;
+});
+
+uppy.on('complete', () => {
+	fileInput.value = null;
+});
+```

+ 151 - 0
docs/sources/screen-capture.mdx

@@ -0,0 +1,151 @@
+---
+sidebar_position: 2
+slug: /screen-capture
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Screen capture
+
+The `@uppy/screen-capture` plugin can record your screen or an application and
+save it as a video.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+When you want users record their screen on their computer. This plugin only
+works on desktop browsers which support
+[`getDisplayMedia API`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia).
+If no support for the API is detected, Screen Capture won’t be installed, so you
+don’t have to do anything.
+
+:::note
+
+To use the screen capture plugin in a Chromium-based browser,
+[your site must be served over https](https://developers.google.com/web/updates/2015/10/chrome-47-webrtc#public_service_announcements).
+This restriction does not apply on `localhost`, so you don’t have to jump
+through many hoops during development.
+
+:::
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/screen-capture
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/screen-capture
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dashboard, ScreenCapture } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dashboard, { inline: true, target: 'body' })
+        uppy.use(ScreenCapture, { target: Uppy.Dashboard })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {3,7,11} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import ScreenCapture from '@uppy/screen-capture';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+import '@uppy/screen-capture/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(ScreenCapture, { target: Dashboard });
+```
+
+### API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'ScreenCapture'`).
+
+#### `title`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs
+(`string`, default: `'Screen Capture'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the screen capture into (`string`
+or `Element`, default: `null`).
+
+#### `displayMediaConstraints`
+
+Options passed to
+[`MediaDevices.getDisplayMedia()`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia)
+(`Object`, default: `null`).
+
+See the
+[`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints)
+for a list of options.
+
+#### `userMediaConstraints`
+
+Options passed to
+[`MediaDevices.getUserMedia()`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
+(`Object`, default: `null`).
+
+See the
+[`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints)
+for a list of options.
+
+#### `preferredVideoMimeType`
+
+Set the preferred mime type for video recordings, for example `'video/webm'`
+(`string`, default: `null`).
+
+If the browser supports the given mime type, the video will be recorded in this
+format. If the browser does not support it, it will use the browser default.
+
+If no preferred video mime type is given, the ScreenCapture plugin will prefer
+types listed in the [`allowedFileTypes` restriction](/docs/uppy/#restrictions),
+if any.
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		startCapturing: 'Begin screen capturing',
+		stopCapturing: 'Stop screen capturing',
+		submitRecordedFile: 'Submit recorded file',
+		streamActive: 'Stream active',
+		streamPassive: 'Stream passive',
+		micDisabled: 'Microphone access denied by user',
+		recording: 'Recording',
+	},
+};
+```

+ 253 - 0
docs/sources/webcam.mdx

@@ -0,0 +1,253 @@
+---
+sidebar_position: 1
+slug: /webcam
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Webcam
+
+The `@uppy/webcam` plugin lets you take photos and record videos with a built-in
+camera on desktop and mobile devices.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use it?
+
+When you want your users to able to use their camera. This plugin is published
+separately but made specifically for the [Dashboard](/docs/dashboard). You can
+technically use it without it, but it’s not officially supported.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/webcam
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/webcam
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dashboard, Webcam } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dashboard, { inline: true, target: 'body' })
+        uppy.use(Webcam, { target: Uppy.Dashboard })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+:::note
+
+To use the Webcam plugin in Chrome,
+[your site must be served over https](https://developers.google.com/web/updates/2015/10/chrome-47-webrtc#public_service_announcements).
+This restriction does not apply on `localhost`, so you don’t have to jump
+through many hoops during development.
+
+:::
+
+```js {3,7,11} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Webcam from '@uppy/webcam';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+import '@uppy/webcam/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(Webcam, { target: Dashboard });
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Webcam'`).
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the webcam into (`string` or
+`Element`, default: `null`).
+
+#### `countdown`
+
+When taking a picture: the amount of seconds to wait before actually taking a
+snapshot (`boolean`, default: `false`).
+
+If set to `false` or 0, the snapshot is taken right away. This also shows a
+`Smile!` message through the [Informer](/docs/informer) before the picture is
+taken.
+
+#### `onBeforeSnapshot`
+
+A hook function to call before a snapshot is taken (`Function`, default:
+`Promise.resolve`).
+
+The Webcam plugin will wait for the returned Promise to resolve before taking
+the snapshot. This can be used to carry out variations on the `countdown` option
+for example.
+
+#### `modes`
+
+The types of recording modes to allow (`Array`, default: `[]`).
+
+- `video-audio` - Record a video file, capturing both audio and video.
+- `video-only` - Record a video file with the webcam, but don’t record audio.
+- `audio-only` - Record an audio file with the user’s microphone.
+- `picture` - Take a picture with the webcam.
+
+By default, all modes are allowed, and the Webcam plugin will show controls for
+recording video as well as taking pictures.
+
+#### `mirror`
+
+Configures whether to mirror preview image from the camera (`boolean`, default:
+`true`).
+
+This option is useful when taking a selfie with a front camera: when you wave
+your right hand, you will see your hand on the right on the preview screen, like
+in the mirror. But when you actually take a picture, it will not be mirrored.
+This is how smartphone selfie cameras behave.
+
+#### `videoConstraints`
+
+Configure the [`MediaTrackConstraints`][] (`Object`, default: `{}`).
+
+You can specify acceptable ranges for the resolution of the video stream using
+the [`aspectRatio`][], [`width`][], and [`height`][] properties. Each property
+takes an object with `{ min, ideal, max }` properties. For example, use
+`width: { min: 720, max: 1920, ideal: 1920 }` to allow any width between 720 and
+1920 pixels wide, while preferring the highest resolution.
+
+Devices sometimes have several cameras, front and back, for example.
+[`facingMode`][] lets you specify which should be used:
+
+- `user`: The video source is facing toward the user; this includes, for
+  example, the front-facing camera on a smartphone.
+- `environment`: The video source is facing away from the user, thereby viewing
+  their environment. This is the back camera on a smartphone.
+- `left`: The video source is facing toward the user but to their left, such as
+  a camera aimed toward the user but over their left shoulder.
+- `right`: The video source is facing toward the user but to their right, such
+  as a camera aimed toward the user but over their right shoulder.
+
+For a full list of available properties, check out MDN documentation for
+[`MediaTrackConstraints`][].
+
+[`mediatrackconstraints`]:
+	https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#Properties_of_video_tracks
+[`aspectratio`]:
+	https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/aspectRatio
+[`width`]:
+	https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/width
+[`height`]:
+	https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/height
+[`facingmode`]:
+	https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode
+
+#### `showVideoSourceDropdown`
+
+Configures whether to show a dropdown which enables to choose the video device
+to use (`boolean`, default: `false`).
+
+This option will have priority over `facingMode` if enabled.
+
+#### `showRecordingLength`
+
+Configures whether to show the length of the recording while the recording is in
+progress (`boolean`, default: `false`).
+
+#### `preferredVideoMimeType`
+
+Set the preferred mime type for video recordings, for example `'video/webm'`
+(`string`, default: `null`).
+
+If the browser supports the given mime type, the video will be recorded in this
+format. If the browser does not support it, it will use the browser default. If
+no preferred video mime type is given, the Webcam plugin will prefer types
+listed in the [`allowedFileTypes` restriction](/docs/uppy/#restrictions), if
+any.
+
+#### `preferredImageMimeType`
+
+Set the preferred mime type for images, for example `'image/png'` (`string`,
+default: `image/jpeg`).
+
+If the browser supports rendering the given mime type, the image will be stored
+in this format. Else `image/jpeg` is used by default. If no preferred image mime
+type is given, the Webcam plugin will prefer types listed in the
+[`allowedFileTypes` restriction](/docs/uppy/#restrictions), if any.
+
+#### `mobileNativeCamera`
+
+Replaces Uppy’s custom camera UI on mobile and tablet with the native device
+camera (`Function: boolean` or `boolean`, default: `isMobile()`).
+
+This will show the “Take Picture” and / or “Record Video” buttons, which ones
+show depends on the [`modes`](#modes) option.
+
+You can set a boolean to forcefully enable / disable this feature, or a function
+which returns a boolean. By default we use the
+[`is-mobile`](https://github.com/juliangruber/is-mobile) package.
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		pluginNameCamera: 'Camera',
+		noCameraTitle: 'Camera Not Available',
+		noCameraDescription:
+			'In order to take pictures or record video, please connect a camera device',
+		recordingStoppedMaxSize:
+			'Recording stopped because the file size is about to exceed the limit',
+		submitRecordedFile: 'Submit recorded file',
+		discardRecordedFile: 'Discard recorded file',
+		// Shown before a picture is taken when the `countdown` option is set.
+		smile: 'Smile!',
+		// Used as the label for the button that takes a picture.
+		// This is not visibly rendered but is picked up by screen readers.
+		takePicture: 'Take a picture',
+		// Used as the label for the button that starts a video recording.
+		// This is not visibly rendered but is picked up by screen readers.
+		startRecording: 'Begin video recording',
+		// Used as the label for the button that stops a video recording.
+		// This is not visibly rendered but is picked up by screen readers.
+		stopRecording: 'Stop video recording',
+		// Used as the label for the recording length counter. See the showRecordingLength option.
+		// This is not visibly rendered but is picked up by screen readers.
+		recordingLength: 'Recording length %{recording_length}',
+		// Title on the “allow access” screen
+		allowAccessTitle: 'Please allow access to your camera',
+		// Description on the “allow access” screen
+		allowAccessDescription:
+			'In order to take pictures or record video with your camera, please allow camera access for this site.',
+	},
+};
+```

+ 4 - 0
docs/uploader/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "Uploaders",
+	"position": 7
+}

+ 522 - 0
docs/uploader/aws-s3-multipart.mdx

@@ -0,0 +1,522 @@
+---
+sidebar_position: 4
+slug: /aws-s3-multipart
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# AWS S3
+
+The `@uppy/aws-s3` plugin can be used to upload files directly to a S3 bucket or
+a S3-compatible provider, such as Google Cloud Storage or DigitalOcean Spaces.
+Uploads can be signed using either [Companion][companion docs], temporary
+credentials, or a custom signing function.
+
+## When should I use it?
+
+:::tip
+
+Not sure which uploader is best for you? Read
+“[Choosing the uploader you need](/docs/guides/choosing-uploader)”.
+
+:::
+
+You can use this plugin when you prefer a _client-to-storage_ over a
+_client-to-server-to-storage_ (such as [Transloadit](/docs/transloadit) or
+[Tus](/docs/tus)) setup. This may in some cases be preferable, for instance, to
+reduce costs or the complexity of running a server and load balancer with
+[Tus](/docs/tus).
+
+Multipart uploads start to become valuable for larger files (100&nbsp;MiB+) as
+it uploads a single object as a set of parts. This has certain benefits, such as
+improved throughput (uploading parts in parallel) and quick recovery from
+network issues (only the failed parts need to be retried). The downside is
+request overhead, as it needs to do creation, signing (unless you are [signing
+on the client][]), and completion requests besides the upload requests. For
+example, if you are uploading files that are only a couple kilobytes with a
+100ms roundtrip latency, you are spending 400ms on overhead and only a few
+milliseconds on uploading.
+
+**In short**
+
+- We recommend to set [`shouldUseMultipart`][] to enable multipart uploads only
+  for large files.
+- If you prefer to have less overhead (+20% upload speed) you can use temporary
+  S3 credentials with [`getTemporarySecurityCredentials`][]. This means users
+  get a single token which allows them to do bucket operations for longer,
+  instead of short lived signed URL per resource. This is a security trade-off.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/aws-s3
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/aws-s3
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, AwsS3 } from "{{UPPY_JS_URL}}"
+        new Uppy().use(AwsS3, { /* see options */ })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+### Setting up your S3 bucket
+
+To use this plugin with S3 we need to setup a bucket with the right permissions
+and CORS settings.
+
+S3 buckets do not allow public uploads for security reasons. To allow Uppy and
+the browser to upload directly to a bucket, its CORS permissions need to be
+configured.
+
+CORS permissions can be found in the
+[S3 Management Console](https://console.aws.amazon.com/s3/home). Click the
+bucket that will receive the uploads, then go into the `Permissions` tab and
+select the `CORS configuration` button. A JSON document will be shown that
+defines the CORS configuration. (AWS used to use XML but now only allow JSON).
+More information about the
+[S3 CORS format here](https://docs.amazonaws.cn/en_us/AmazonS3/latest/userguide/ManageCorsUsing.html).
+
+The configuration required for Uppy and Companion is this:
+
+```json
+[
+	{
+		"AllowedOrigins": ["https://my-app.com"],
+		"AllowedMethods": ["GET", "PUT"],
+		"MaxAgeSeconds": 3000,
+		"AllowedHeaders": [
+			"Authorization",
+			"x-amz-date",
+			"x-amz-content-sha256",
+			"content-type"
+		],
+		"ExposeHeaders": ["ETag", "Location"]
+	},
+	{
+		"AllowedOrigins": ["*"],
+		"AllowedMethods": ["GET"],
+		"MaxAgeSeconds": 3000
+	}
+]
+```
+
+A good practice is to use two CORS rules: one for viewing the uploaded files,
+and one for uploading files. This is done above where the first object in the
+array defines the rules for uploading, and the second for viewing. The example
+above **makes files publicly viewable**. You can change it according to your
+needs.
+
+If you are using an IAM policy to allow access to the S3 bucket, the policy must
+have at least the `s3:PutObject` and `s3:PutObjectAcl` permissions scoped to the
+bucket in question. In-depth documentation about CORS rules is available on the
+[AWS documentation site](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html).
+
+### Use with your own server
+
+The recommended approach is to integrate `@uppy/aws-s3` with your own server.
+You will need to do the following things:
+
+1. [Setup a S3 bucket](#setting-up-your-s3-bucket).
+2. [Setup your server](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/index.js)
+3. [Setup Uppy client](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/public/index.html).
+
+### Use with Companion
+
+[Companion](/docs/companion) has S3 routes built-in for a plug-and-play
+experience with Uppy.
+
+:::caution
+
+Generally it’s better for access control, observability, and scaling to
+integrate `@uppy/aws-s3` with your own server. You may want to use
+[Companion](/docs/companion) for creating, signing, and completing your S3
+uploads if you already need Companion for remote files (such as from Google
+Drive). Otherwise it’s not worth the hosting effort.
+
+:::
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import AwsS3 from '@uppy/aws-s3';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+const uppy = new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(AwsS3, {
+		shouldUseMultipart: (file) => file.size > 100 * 2 ** 20,
+		companionUrl: 'https://companion.uppy.io',
+	});
+```
+
+## API
+
+### Options
+
+#### `shouldUseMultipart(file)`
+
+:::warning
+
+Until the next major version, not setting this option uses the
+[legacy version of this plugin](../aws-s3/). This is a suboptimal experience for
+some of your user’s uploads. It’s best for speed and stability to upload large
+(100&nbsp;MiB+) files with multipart and small files with regular uploads.
+
+:::
+
+A boolean, or a function that returns a boolean which is called for each file
+that is uploaded with the corresponding `UppyFile` instance as argument.
+
+By default, all files are uploaded as multipart. In a future version, all files
+with a `file.size` ≤ 100&nbsp;MiB will be uploaded in a single chunk, all files
+larger than that as multipart.
+
+Here’s how to use it:
+
+```js
+uppy.use(AwsS3, {
+	shouldUseMultipart(file) {
+		// Use multipart only for files larger than 100MiB.
+		return file.size > 100 * 2 ** 20;
+	},
+});
+```
+
+#### `limit`
+
+The maximum amount of files to upload in parallel (`number`, default: `6`).
+
+Note that the amount of files is not the same as the amount of concurrent
+connections. Multipart uploads can use many requests per file. For example, for
+a 100 MiB file with a part size of 5 MiB:
+
+- 1 `createMultipartUpload` request
+- 100/5 = 20 sign requests (unless you are [signing on the client][])
+- 100/5 = 20 upload requests
+- 1 `completeMultipartUpload` request
+
+:::caution
+
+Unless you have a good reason and are well informed about the average internet
+speed of your users, do not set this higher. S3 uses HTTP/1.1, which means a
+limit to concurrent connections and your uploads may expire before they are
+uploaded.
+
+:::
+
+#### `companionUrl`
+
+URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `companionCookiesRule`
+
+This option correlates to the
+[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
+(`string`, default: `'same-origin'`).
+
+This tells the plugin whether to send cookies to [Companion](/docs/companion).
+
+#### `retryDelays`
+
+`retryDelays` are the intervals in milliseconds used to retry a failed chunk
+(`array`, default: `[0, 1000, 3000, 5000]`).
+
+This is also used for [`signPart()`](#signpartfile-partdata). Set to `null` to
+disable automatic retries, and fail instantly if any chunk fails to upload.
+
+#### `getChunkSize(file)`
+
+A function that returns the minimum chunk size to use when uploading the given
+file.
+
+The S3 Multipart plugin uploads files in chunks. Chunks are sent in batches to
+have presigned URLs generated with [`signPart()`](#signpartfile-partdata). To
+reduce the amount of requests for large files, you can choose a larger chunk
+size, at the cost of having to re-upload more data if one chunk fails to upload.
+
+S3 requires a minimum chunk size of 5MiB, and supports at most 10,000 chunks per
+multipart upload. If `getChunkSize()` returns a size that’s too small, Uppy will
+increase it to S3’s minimum requirements.
+
+#### `getUploadParameters(file, options)`
+
+:::note
+
+When using [Companion][companion docs] to sign S3 uploads, you should not define
+this option.
+
+:::
+
+A function that will be called for each non-multipart upload.
+
+- `file`: `UppyFile` the file that will be uploaded
+- `options`: `object`
+  - `signal`: `AbortSignal`
+- **Returns:** `object | Promise<object>`
+  - `method`: `string`, the HTTP method to be used for the upload. This should
+    be one of either `PUT` or `POST`, depending on the type of upload used.
+  - `url`: `string`, the URL to which the upload request will be sent. When
+    using a presigned PUT upload, this should be the URL to the S3 object with
+    signing parameters included in the query string. When using a POST upload
+    with a policy document, this should be the root URL of the bucket.
+  - `fields` `object`, an object with form fields to send along with the upload
+    request. For presigned PUT uploads (which are default), this should be left
+    empty.
+  - `headers`: `object`, an object with request headers to send along with the
+    upload request. When using a presigned PUT upload, it’s a good idea to
+    provide `headers['content-type']`. That will make sure that the request uses
+    the same content-type that was used to generate the signature. Without it,
+    the browser may decide on a different content-type instead, causing S3 to
+    reject the upload.
+
+#### `createMultipartUpload(file)`
+
+A function that calls the S3 Multipart API to create a new upload.
+
+`file` is the file object from Uppy’s state. The most relevant keys are
+`file.name` and `file.type`.
+
+Return a Promise for an object with keys:
+
+- `uploadId` - The UploadID returned by S3.
+- `key` - The object key for the file. This needs to be returned to allow it to
+  be different from the `file.name`.
+
+The default implementation calls out to Companion’s S3 signing endpoints.
+
+#### `listParts(file, { uploadId, key })`
+
+A function that calls the S3 Multipart API to list the parts of a file that have
+already been uploaded.
+
+Receives the `file` object from Uppy’s state, and an object with keys:
+
+- `uploadId` - The UploadID of this Multipart upload.
+- `key` - The object key of this Multipart upload.
+
+Return a Promise for an array of S3 Part objects, as returned by the S3
+Multipart API. Each object has keys:
+
+- `PartNumber` - The index in the file of the uploaded part.
+- `Size` - The size of the part in bytes.
+- `ETag` - The ETag of the part, used to identify it when completing the
+  multipart upload and combining all parts into a single file.
+
+The default implementation calls out to Companion’s S3 signing endpoints.
+
+#### `signPart(file, partData)`
+
+A function that generates a signed URL for the specified part number. The
+`partData` argument is an object with the keys:
+
+- `uploadId` - The UploadID of this Multipart upload.
+- `key` - The object key in the S3 bucket.
+- `partNumber` - can’t be zero.
+- `body` – The data that will be signed.
+- `signal` – An `AbortSignal` that may be used to abort an ongoing request.
+
+This function should return a object, or a promise that resolves to an object,
+with the following keys:
+
+- `url` – the presigned URL, as a `string`.
+- `headers` – **(Optional)** Custom headers to send along with the request to S3
+  endpoint.
+
+An example of what the return value should look like:
+
+```json
+{
+	"url": "https://bucket.region.amazonaws.com/path/to/file.jpg?partNumber=1&...",
+	"headers": { "Content-MD5": "foo" }
+}
+```
+
+#### `abortMultipartUpload(file, { uploadId, key })`
+
+A function that calls the S3 Multipart API to abort a Multipart upload, and
+removes all parts that have been uploaded so far.
+
+Receives the `file` object from Uppy’s state, and an object with keys:
+
+- `uploadId` - The UploadID of this Multipart upload.
+- `key` - The object key of this Multipart upload.
+
+This is typically called when the user cancels an upload. Cancellation cannot
+fail in Uppy, so the result of this function is ignored.
+
+The default implementation calls out to Companion’s S3 signing endpoints.
+
+#### `completeMultipartUpload(file, { uploadId, key, parts })`
+
+A function that calls the S3 Multipart API to complete a Multipart upload,
+combining all parts into a single object in the S3 bucket.
+
+Receives the `file` object from Uppy’s state, and an object with keys:
+
+- `uploadId` - The UploadID of this Multipart upload.
+- `key` - The object key of this Multipart upload.
+- `parts` - S3-style list of parts, an array of objects with `ETag` and
+  `PartNumber` properties. This can be passed straight to S3’s Multipart API.
+
+Return a Promise for an object with properties:
+
+- `location` - **(Optional)** A publicly accessible URL to the object in the S3
+  bucket.
+
+The default implementation calls out to Companion’s S3 signing endpoints.
+
+#### `allowedMetaFields: null`
+
+Pass an array of field names to limit the metadata fields that will be added to
+upload as query parameters.
+
+- Set this to `['name']` to only send the `name` field.
+- Set this to `null` (the default) to send _all_ metadata fields.
+- Set this to an empty array `[]` to not send any fields.
+
+<details>
+<summary>Deprecated options</summary>
+
+#### `prepareUploadParts(file, partData)`
+
+A function that generates a batch of signed URLs for the specified part numbers.
+
+Receives the `file` object from Uppy’s state. The `partData` argument is an
+object with keys:
+
+- `uploadId` - The UploadID of this Multipart upload.
+- `key` - The object key in the S3 bucket.
+- `parts` - An array of objects with the part number and chunk
+  (`Array<{ number: number, chunk: blob }>`). `number` can’t be zero.
+
+`prepareUploadParts` should return a `Promise` with an `Object` with keys:
+
+- `presignedUrls` - A JavaScript object with the part numbers as keys and the
+  presigned URL for each part as the value.
+- `headers` - **(Optional)** Custom headers to send along with every request per
+  part (`{ 1: { 'Content-MD5': 'hash' }}`). These are (1-based) indexed by part
+  number too so you can for instance send the MD5 hash validation for each part
+  to S3.
+
+An example of what the return value should look like:
+
+```json
+{
+	"presignedUrls": {
+		"1": "https://bucket.region.amazonaws.com/path/to/file.jpg?partNumber=1&...",
+		"2": "https://bucket.region.amazonaws.com/path/to/file.jpg?partNumber=2&...",
+		"3": "https://bucket.region.amazonaws.com/path/to/file.jpg?partNumber=3&..."
+	},
+	"headers": {
+		"1": { "Content-MD5": "foo" },
+		"2": { "Content-MD5": "bar" },
+		"3": { "Content-MD5": "baz" }
+	}
+}
+```
+
+If an error occurred, reject the `Promise` with an `Object` with the following
+keys:
+
+```json
+{ "source": { "status": 500 } }
+```
+
+`status` is the HTTP code and is required for determining whether to retry the
+request. `prepareUploadParts` will be retried if the code is `0`, `409`, `423`,
+or between `500` and `600`.
+
+</details>
+
+#### `getTemporarySecurityCredentials(options)`
+
+:::note
+
+When using [Companion][companion docs] as a backend, you can pass `true` instead
+of a function. Setting up Companion will not simplify the process of getting
+signing on the client.
+
+:::
+
+A boolean (when using Companion), or an (async) function to retrieve temporary
+security credentials used for all uploads instead of signing every part. This
+results in less request overhead which can lead to around 20% faster uploads.
+This is a security tradeoff. We recommend to not use this option unless you are
+familiar with the security implications of temporary credentials, and how to
+setup your bucket to make it work. See the
+[Requesting temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_request.html)
+AWS guide for more information.
+
+It’s strongly recommended to have some sort of caching process to avoid
+requesting more temporary token than necessary.
+
+- `options`: `object`
+  - `signal`: `AbortSignal`
+- **Returns:** `object | Promise<object>`
+  - `credentials`: `object`
+    - `AccessKeyId`: `string`
+    - `SecretAccessKey`: `string`
+    - `SessionToken`: `string`
+    - `Expiration`: `string`
+  - `bucket`: `string`
+  - `region`: `string`
+
+If you are using Companion (for example because you want to support remote
+upload sources), you can pass a boolean:
+
+```js
+uppy.use(AwsS3, {
+	// This is an example using Companion:
+	companionUrl: 'http://companion.uppy.io',
+	getTemporarySecurityCredentials: true,
+	shouldUseMultipart: (file) => file.size > 100 * 2 ** 20,
+});
+```
+
+In the most common case, you are using a different backend, in which case you
+need to specify a function:
+
+```js
+uppy.use(AwsS3, {
+	// This is an example not using Companion:
+	async getTemporarySecurityCredentials({ signal }) {
+		const response = await fetch('/sts-token', { signal });
+		if (!response.ok)
+			throw new Error('Failed to fetch STS', { cause: response });
+		return response.json();
+	},
+	shouldUseMultipart: (file) => file.size > 100 * 2 ** 20,
+});
+```
+
+[`gettemporarysecuritycredentials`]: #gettemporarysecuritycredentialsoptions
+[`shouldusemultipart`]: #shouldusemultipartfile
+[companion docs]: /docs/companion
+[signing on the client]: #gettemporarysecuritycredentialsoptions

+ 463 - 0
docs/uploader/aws-s3.mdx

@@ -0,0 +1,463 @@
+---
+sidebar_position: 3
+slug: /aws-s3
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# AWS S3 (legacy)
+
+The `@uppy/aws-s3` plugin can be used to upload files directly to a S3 bucket or
+a S3-compatible provider, such as Google Cloud Storage or DigitalOcean Spaces.
+Uploads can be signed using either [Companion][companion docs] or a custom
+signing function.
+
+This documents the legacy version of this plugin that we plan to remove on the
+next version.
+
+## When should I use it?
+
+:::tip
+
+Not sure which uploader is best for you? Read
+“[Choosing the uploader you need](/docs/guides/choosing-uploader)”.
+
+:::
+
+:::warning
+
+This plugin is deprecated, you should switch to using the
+[modern version of this plugin](/docs/aws-s3-multipart).
+
+:::
+
+You can use this plugin when you prefer a _client-to-storage_ over a
+_client-to-server-to-storage_ (such as [Transloadit](/docs/transloadit) or
+[Tus](/docs/tus)) setup. This may in some cases be preferable, for instance, to
+reduce costs or the complexity of running a server and load balancer with
+[Tus](/docs/tus).
+
+This plugin can be used with AWS S3, DigitalOcean Spaces, Google Cloud Storage,
+or any S3-compatible provider. Although all S3-compatible providers are
+supported, we don’t test against them, this plugin was developed against S3 so a
+small risk is involved in using the others.
+
+`@uppy/aws-s3` is best suited for small files and/or lots of files. If you are
+planning to upload mostly large files (100&nbsp;MB+), consider using
+[`@uppy/aws-s3-multipart`](/docs/aws-s3-multipart).
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/aws-s3
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/aws-s3
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, AwsS3 } from "{{UPPY_JS_URL}}"
+        new Uppy().use(AwsS3, { /* see options */ })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+A quick overview of the complete API.
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import AwsS3 from '@uppy/aws-s3';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+const uppy = new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(AwsS3, { companionUrl: 'http://companion.uppy.io' });
+```
+
+### With a AWS S3 bucket
+
+To use this plugin with S3 we need to setup a bucket with the right permissions
+and CORS settings.
+
+S3 buckets do not allow public uploads for security reasons. To allow Uppy and
+the browser to upload directly to a bucket, its CORS permissions need to be
+configured.
+
+CORS permissions can be found in the
+[S3 Management Console](https://console.aws.amazon.com/s3/home). Click the
+bucket that will receive the uploads, then go into the `Permissions` tab and
+select the `CORS configuration` button. A JSON document will be shown that
+defines the CORS configuration. (AWS used to use XML but now only allow JSON).
+More information about the
+[S3 CORS format here](https://docs.amazonaws.cn/en_us/AmazonS3/latest/userguide/ManageCorsUsing.html).
+
+The configuration required for Uppy and Companion is this:
+
+```json
+[
+	{
+		"AllowedOrigins": ["https://my-app.com"],
+		"AllowedMethods": ["GET", "POST"],
+		"MaxAgeSeconds": 3000,
+		"AllowedHeaders": [
+			"Authorization",
+			"x-amz-date",
+			"x-amz-content-sha256",
+			"content-type"
+		]
+	},
+	{
+		"AllowedOrigins": ["*"],
+		"AllowedMethods": ["GET"],
+		"MaxAgeSeconds": 3000
+	}
+]
+```
+
+A good practice is to use two CORS rules: one for viewing the uploaded files,
+and one for uploading files. This is done above where the first object in the
+array defines the rules for uploading, and the second for viewing. The example
+above **makes files publicly viewable**. You can change it according to your
+needs.
+
+If you are using an IAM policy to allow access to the S3 bucket, the policy must
+have at least the `s3:PutObject` and `s3:PutObjectAcl` permissions scoped to the
+bucket in question. In-depth documentation about CORS rules is available on the
+[AWS documentation site](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html).
+
+### With a DigitalOcean Spaces bucket
+
+:::tip
+
+Checkout the
+[DigitalOcean Spaces example](https://github.com/transloadit/uppy/tree/main/examples/digitalocean-spaces)
+in the Uppy repository for a complete, runnable example.
+
+:::
+
+DigitalOcean Spaces is S3-compatible so you only need to change the endpoint and
+bucket. Make sure you have a `key` and `secret`. If not, refer to
+“[How To Create a DigitalOcean Space and API Key](https://www.digitalocean.com/community/tutorials/how-to-create-a-digitalocean-space-and-api-key)”.
+
+When using [Companion](/docs/companion) as standalone, you can set these as
+environment variables:
+
+```bash
+export COMPANION_AWS_KEY="xxx"
+export COMPANION_AWS_SECRET="xxx"
+export COMPANION_AWS_REGION="us-east-1"
+export COMPANION_AWS_ENDPOINT="https://{region}.digitaloceanspaces.com"
+export COMPANION_AWS_BUCKET="my-space-name"
+```
+
+The `{region}` string will be replaced by the contents of the
+`COMPANION_AWS_REGION` environment variable.
+
+When using [Companion](/docs/companion) as an Express integration, configure the
+`s3` options:
+
+```js
+const options = {
+	s3: {
+		key: 'xxx',
+		secret: 'xxx',
+		bucket: 'my-space-name',
+		region: 'us-east-1',
+		endpoint: 'https://{region}.digitaloceanspaces.com',
+	},
+};
+```
+
+### With a Google Cloud Storage bucket
+
+For the `@uppy/aws-s3` plugin to be able to upload to a GCS bucket, it needs the
+Interoperability setting enabled. You can enable the Interoperability setting
+and
+[generate interoperable storage access keys](https://cloud.google.com/storage/docs/migrating#keys)
+by going to [Google Cloud Storage](https://console.cloud.google.com/storage) »
+Settings » Interoperability. Then set the environment variables for Companion
+like this:
+
+```bash
+export COMPANION_AWS_ENDPOINT="https://storage.googleapis.com"
+export COMPANION_AWS_BUCKET="YOUR-GCS-BUCKET-NAME"
+export COMPANION_AWS_KEY="GOOGxxxxxxxxx" # The Access Key
+export COMPANION_AWS_SECRET="YOUR-GCS-SECRET" # The Secret
+```
+
+You do not need to configure the region with GCS.
+
+You also need to configure CORS with their HTTP API. If you haven’t done this
+already, see
+[Configuring CORS on a Bucket](https://cloud.google.com/storage/docs/configuring-cors#Configuring-CORS-on-a-Bucket)
+in the GCS documentation, or follow the steps below to do it using Google’s API
+playground.
+
+The JSON format consists of an array of CORS configuration objects. For
+instance:
+
+```json
+{
+	"cors": [
+		{
+			"origin": ["https://my-app.com"],
+			"method": ["GET", "POST"],
+			"maxAgeSeconds": 3000
+		},
+		{
+			"origin": ["*"],
+			"method": ["GET"],
+			"maxAgeSeconds": 3000
+		}
+	]
+}
+```
+
+When using presigned `PUT` uploads, replace the `"POST"` method by `"PUT"` in
+the first entry.
+
+If you have the [gsutil](https://cloud.google.com/storage/docs/gsutil)
+command-line tool, you can apply this configuration using the
+[gsutil cors](https://cloud.google.com/storage/docs/configuring-cors#configure-cors-bucket)
+command.
+
+```bash
+gsutil cors set THAT-FILE.json gs://BUCKET-NAME
+```
+
+Otherwise, you can manually apply it through the OAuth playground:
+
+1.  Get a temporary API token from the
+    [Google OAuth2.0 playground](https://developers.google.com/oauthplayground/)
+2.  Select the `Cloud Storage JSON API v1` » `devstorage.full_control` scope
+3.  Press `Authorize APIs` and allow access
+4.  Click `Step 3 - Configure request to API`
+5.  Configure it as follows:
+    1.  HTTP Method: PATCH
+    2.  Request URI: `https://www.googleapis.com/storage/v1/b/YOUR_BUCKET_NAME`
+    3.  Content-Type: application/json (should be the default)
+    4.  Press `Enter request body` and input your CORS configuration
+6.  Press `Send the request`.
+
+### Use with your own server
+
+The recommended approach is to integrate `@uppy/aws-s3` with your own server.
+You will need to do the following things:
+
+1. Setup a bucket
+2. Create endpoints in your server. You can create them as edge functions (such
+   as AWS Lambdas), inside Next.js as an API route, or wherever your server runs
+   - `POST` > `/uppy/s3`: get upload parameters
+3. [Setup Uppy](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/public/index.html)
+
+### Use with Companion
+
+[Companion](/docs/companion) has S3 routes built-in for a plug-and-play
+experience with Uppy.
+
+:::caution
+
+Generally it’s better for access control, observability, and scaling to
+integrate `@uppy/aws-s3` with your own server. You may want to use
+[Companion](/docs/companion) for creating, signing, and completing your S3
+uploads if you already need Companion for remote files (such as from Google
+Drive). Otherwise it’s not worth the hosting effort.
+
+:::
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import AwsS3 from '@uppy/aws-s3';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+const uppy = new Uppy.use(Dashboard, { inline: true, target: 'body' }).use(
+	AwsS3,
+	{ companionUrl: 'http://companion.uppy.io' },
+);
+```
+
+## Options
+
+The `@uppy/aws-s3` plugin has the following configurable options:
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'AwsS3'`).
+
+#### `companionUrl`
+
+Companion instance to use for signing S3 uploads (`string`, default: `null`).
+
+#### `companionHeaders`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on
+every request (`Object`, default: `{}`).
+
+#### `allowedMetaFields`
+
+Pass an array of field names to limit the metadata fields that will be added to
+upload as query parameters (`Array`, default: `null`).
+
+- Set this to `['name']` to only send the `name` field.
+- Set this to `null` (the default) to send _all_ metadata fields.
+- Set this to an empty array `[]` to not send any fields.
+
+#### `getUploadParameters(file)`
+
+:::note
+
+When using [Companion][companion docs] to sign S3 uploads, do not define this
+option.
+
+:::
+
+A function that returns upload parameters for a file (`Promise`, default:
+`null`).
+
+Parameters should be returned as an object, or as a `Promise` that fulfills with
+an object, with keys `{ method, url, fields, headers }`.
+
+- The `method` field is the HTTP method to be used for the upload. This should
+  be one of either `PUT` or `POST`, depending on the type of upload used.
+- The `url` field is the URL to which the upload request will be sent. When
+  using a presigned PUT upload, this should be the URL to the S3 object with
+  signing parameters included in the query string. When using a POST upload with
+  a policy document, this should be the root URL of the bucket.
+- The `fields` field is an object with form fields to send along with the upload
+  request. For presigned PUT uploads, this should be left empty.
+- The `headers` field is an object with request headers to send along with the
+  upload request. When using a presigned PUT upload, it’s a good idea to provide
+  `headers['content-type']`. That will make sure that the request uses the same
+  content-type that was used to generate the signature. Without it, the browser
+  may decide on a different content-type instead, causing S3 to reject the
+  upload.
+
+#### `timeout`
+
+When no upload progress events have been received for this amount of
+milliseconds, assume the connection has an issue and abort the upload (`number`,
+default: `30_000`).
+
+This is passed through to [XHRUpload](/docs/xhr-upload#timeout-30-1000); see its
+documentation page for details. Set to `0` to disable this check.
+
+#### `limit`
+
+Limit the amount of uploads going on at the same time (`number`, default: `5`).
+
+Setting this to `0` means no limit on concurrent uploads, but we recommend a
+value between `5` and `20`.
+
+#### `getResponseData(responseText, response)`
+
+:::note
+
+This is an advanced option intended for use with _almost_ S3-compatible storage
+solutions.
+
+:::
+
+Customize response handling once an upload is completed. This passes the
+function through to @uppy/xhr-upload, see its
+[documentation](https://uppy.io/docs/xhr-upload/#getResponseData-responseText-response)
+for API details.
+
+This option is useful when uploading to an S3-like service that doesn’t reply
+with an XML document, but with something else such as JSON.
+
+#### `locale: {}`
+
+```js
+export default {
+	strings: {
+		timedOut: 'Upload stalled for %{seconds} seconds, aborting.',
+	},
+};
+```
+
+## Frequently Asked Questions
+
+### How can I generate a presigned URL server-side?
+
+The `getUploadParameters` function can return a `Promise`, so upload parameters
+can be prepared server-side. That way, no private keys to the S3 bucket need to
+be shared on the client. For example, there could be a PHP server endpoint that
+prepares a presigned URL for a file:
+
+```js
+uppy.use(AwsS3, {
+	getUploadParameters(file) {
+		// Send a request to our PHP signing endpoint.
+		return fetch('/s3-sign.php', {
+			method: 'post',
+			// Send and receive JSON.
+			headers: {
+				accept: 'application/json',
+				'content-type': 'application/json',
+			},
+			body: JSON.stringify({
+				filename: file.name,
+				contentType: file.type,
+			}),
+		})
+			.then((response) => {
+				// Parse the JSON response.
+				return response.json();
+			})
+			.then((data) => {
+				// Return an object in the correct shape.
+				return {
+					method: data.method,
+					url: data.url,
+					fields: data.fields,
+					// Provide content type header required by S3
+					headers: {
+						'Content-Type': file.type,
+					},
+				};
+			});
+	},
+});
+```
+
+See either the
+[aws-nodejs](https://github.com/transloadit/uppy/tree/HEAD/examples/aws-nodejs)
+or [aws-php](https://github.com/transloadit/uppy/tree/HEAD/examples/aws-php)
+examples in the uppy repository for a demonstration of how to implement handling
+of presigned URLs on both the server-side and client-side.
+
+### How can I retrieve the presigned parameters of the uploaded file?
+
+Once the file is uploaded, it’s possible to retrieve the parameters that were
+generated in `getUploadParameters(file)` via the `file.meta` field:
+
+```js
+uppy.on('upload-success', (file, data) => {
+	const s3Key = file.meta['key']; // the S3 object key of the uploaded file
+});
+```
+
+[companion docs]: /docs/companion

+ 684 - 0
docs/uploader/transloadit.mdx

@@ -0,0 +1,684 @@
+---
+sidebar_position: 1
+slug: /transloadit
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Transloadit
+
+The `@uppy/transloadit` plugin can be used to upload files directly to
+[Transloadit](https://transloadit.com/) for all kinds of processing, such as
+transcoding video, resizing images, zipping/unzipping, [and much
+more][transloadit-services].
+
+## When should I use it?
+
+:::tip
+
+Not sure which uploader is best for you? Read
+“[Choosing the uploader you need](/docs/guides/choosing-uploader)”.
+
+:::
+
+Transloadit’s strength is versatility. By doing video, audio, images, documents,
+and more, you only need one vendor for [all your file processing
+needs][transloadit-services]. The `@uppy/transloadit` plugin directly uploads to
+Transloadit so you only have to worry about creating a
+[Template][transloadit-concepts]. Transloadit accepts the files, processes
+according to the instructions in the Template, and stores the results in storage
+of your choosing, such as a self-owned S3 bucket. The Transloadit plugin uses
+[Tus](/docs/tus) under the hood so you don’t have to sacrifice reliable,
+resumable uploads.
+
+You should use `@uppy/transloadit` if you don’t want to host your own Tus or
+Companion servers, (optionally) need file processing, and store it in the
+service (such as S3 or GCS) of your liking. All with minimal effort.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/transloadit
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/transloadit
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Transloadit } from "{{UPPY_JS_URL}}"
+        new Uppy().use(Transloadit, { /* see options */ })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+A quick overview of the complete API.
+
+```js {10-17} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Transloadit from '@uppy/transloadit';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+const uppy = new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(Transloadit, {
+		assemblyOptions: {
+			params: {
+				auth: { key: 'your-transloadit-key' },
+				template_id: 'your-template-id',
+			},
+		},
+	});
+// Optionally listen to events
+uppy.on('transloadit:assembly-created', (assembly, fileIDs) => {});
+uppy.on('transloadit:upload', (file, assembly) => {});
+uppy.on('transloadit:assembly-executing', (assembly) => {});
+uppy.on('transloadit:result', (stepName, result, assembly) => {});
+uppy.on('transloadit:complete', (assembly) => {});
+```
+
+### Use with Companion
+
+:::note
+
+All [Transloadit plans](https://transloadit/pricing/) come with a hosted version
+of Companion.
+
+:::
+
+You can use this plugin together with Transloadit’s hosted Companion service to
+let your users import files from third party sources across the web. To do so
+each provider plugin must be configured with Transloadit’s Companion URLs:
+
+```js
+import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
+import Dropbox from '@uppy/dropbox';
+
+uppy.use(Dropbox, {
+	companionUrl: COMPANION_URL,
+	companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
+});
+```
+
+This will already work. Transloadit’s OAuth applications are used to
+authenticate your users by default. Your users will be asked to provide
+Transloadit access to their files. Since your users are probably not aware of
+Transloadit, this may be confusing or decrease trust. You may also hit rate
+limits, because the OAuth application is shared between everyone using
+Transloadit.
+
+To solve that, you can use your own OAuth keys with Transloadit’s hosted
+Companion servers by using Transloadit Template Credentials. [Create a Template
+Credential][template-credentials] on the Transloadit site. Select “Companion
+OAuth” for the service, and enter the key and secret for the provider you want
+to use. Then you can pass the name of the new credentials to that provider:
+
+```js
+import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
+import Dropbox from '@uppy/dropbox';
+
+uppy.use(Dropbox, {
+	companionUrl: COMPANION_URL,
+	companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
+	companionKeysParams: {
+		key: 'YOUR_TRANSLOADIT_API_KEY',
+		credentialsName: 'my_companion_dropbox_creds',
+	},
+});
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Transloadit'`).
+
+#### `service`
+
+The Transloadit API URL to use (`string`, default:
+`https://api2.transloadit.com`).
+
+The default will try to route traffic efficiently based on the location of your
+users. You could for instance set it to `https://api2-us-east-1.transloadit.com`
+if you need the traffic to stay inside a particular region.
+
+#### `limit`
+
+Limit the amount of uploads going on at the same time (`number`, default: `20`).
+
+Setting this to `0` means no limit on concurrent uploads, but we recommend a
+value between `5` and `20`. This option is passed through to the
+[`@uppy/tus`](/docs/tus) plugin, which this plugin uses internally.
+
+#### `assemblyOptions`
+
+Configure the
+[Assembly Instructions](https://transloadit.com/docs/topics/assembly-instructions/),
+the fields to send along to the assembly, and authentication
+(`object | function`, default: `null`).
+
+The object you can pass or return from a function has this structure:
+
+```js
+{
+  params: {
+    auth: { key: 'key-from-transloadit' },
+    template_id: 'id-from-transloadit',
+    steps: {
+      // Overruling Template at runtime
+    },
+    notify_url: 'https://your-domain.com/assembly-status',
+  },
+  signature: 'generated-signature',
+  fields: {
+    // Dynamic or static fields to send along
+  },
+}
+```
+
+- `params` is used to authenticate with Transloadit and using your desired
+  [template](https://transloadit.com/docs/topics/templates/).
+  - `auth.key` _(required)_ is your authentication key which you can find on the
+    “Credentials” page of your account.
+  - `template_id` _(required)_ is the unique identifier to use the right
+    template from your account.
+  - `steps` _(optional)_ can be used to
+    [overrule Templates at runtime](https://transloadit.com/docs/topics/templates/#overruling-templates-at-runtime).
+    A typical use case might be changing the storage path on the fly based on
+    the session user id. For most use cases, we recommend to let your Templates
+    handle dynamic cases (they can accept `fields` and execute arbitrary
+    JavaScript as well), and not pass in `steps` from a browser. The template
+    editor also has extra validations and context.
+  - `notify_url` _(optional)_ is a pingback with the assembly status as JSON.
+    For instance, if you don’t want to block the user experience by letting them
+    wait for your template to complete with
+    [`waitForEncoding`](#waitForEncoding), but you do want to want to
+    asynchrounously have an update, you can provide an URL which will be
+    “pinged” with the assembly status.
+- `signature` _(optional, but recommended)_ is a cryptographic signature to
+  provide further trust in unstrusted environments. Refer to
+  “[Signature Authentication”](https://transloadit.com/docs/topics/signature-authentication/)
+  for more information.
+- `fields` _(optional)_ can be used to to send along key/value pairs, which can
+  be
+  [used dynamically in your template](https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions).
+
+<details>
+  <summary>Examples</summary>
+
+**As a function**
+
+A custom `assemblyOptions()` option should return an object or a promise for an
+object.
+
+```js
+uppy.use(Transloadit, {
+	assemblyOptions(file) {
+		return {
+			params: {
+				auth: { key: 'TRANSLOADIT_AUTH_KEY_HERE' },
+				template_id: 'xyz',
+			},
+			fields: {
+				caption: file.meta.caption,
+			},
+		};
+	},
+});
+```
+
+The `${fields.caption}` variable will be available in the Assembly spawned from
+Template `xyz`. You can use this to dynamically watermark images for example.
+
+`assemblyOptions()` may also return a Promise, so it could retrieve signed
+Assembly parameters from a server. For example, assuming an endpoint
+`/transloadit-params` that responds with a JSON object with
+`{ params, signature }` properties:
+
+```js
+uppy.use(Transloadit, {
+	async assemblyOptions(file) {
+		const res = await fetch('/transloadit-params');
+		return response.json();
+	},
+});
+```
+
+**As an object**
+
+If you don’t need to change anything dynamically, you can also pass an object
+directly.
+
+```js
+uppy.use(Transloadit, {
+	assemblyOptions: {
+		params: { auth: { key: 'transloadit-key' } },
+	},
+});
+```
+
+**Use with @uppy/form**
+
+Combine the `assemblyOptions()` option with the [Form](/docs/form) plugin to
+pass user input from a `<form>` to a Transloadit Assembly:
+
+```js
+// This will add form field values to each file's `.meta` object:
+uppy.use(Form, { getMetaFromForm: true });
+uppy.use(Transloadit, {
+	getAssemblyOptions(file) {
+		return {
+			params: {
+				/* ... */
+			},
+			// Pass through the fields you need:
+			fields: {
+				message: file.meta.message,
+			},
+		};
+	},
+});
+```
+
+</details>
+
+:::caution
+
+When you go to production always make sure to set the `signature`. **Not using
+[Signature Authentication](https://transloadit.com/docs/topics/signature-authentication/)
+can be a security risk**. Signature Authentication is a security measure that
+can prevent outsiders from tampering with your Assembly Instructions. While
+Signature Authentication is not implemented (yet), we recommend to disable
+`allow_steps_override` in your Templates to avoid outsiders being able to pass
+in any Instructions and storage targets on your behalf.
+
+:::
+
+#### `waitForEncoding`
+
+Wait for the template to finish, rather than only the upload, before marking the
+upload complete (`boolean`, default: `false`).
+
+- When `false`, the Assemblies will complete (or error) in the background but
+  Uppy won’t know or care about it. You may have to let Transloadit ping you via
+  a `notify_url` and asynchronously inform your user (email, in-app
+  notification).
+- When `true`, the Transloadit plugin waits for Assemblies to complete before
+  the files are marked as completed. This means users have to wait for a
+  potentially long time, depending on how complicated your Assembly instructions
+  are. But, you can receive the final status and transcoding results on the
+  client side with less effort.
+
+When this is enabled, you can listen for the
+[`transloadit:result`](#transloaditresult) and
+[`transloadit:complete`](#transloaditcomplete) events.
+
+#### `waitForMetadata`
+
+Wait for Transloadit’s backend to catch early errors, not the entire Assembly to
+complete. (`boolean`, default: `false`)
+
+When set to `true`, the Transloadit plugin waits for Transloadit’s backend to
+extract metadata from all the uploaded files. This is mostly handy if you want
+to have a quick user experience (so your users don’t necessarily need to wait
+for all the encoding to complete), but you do want to let users know about some
+types of errors that can be caught early on, like file format issues.
+
+You you can listen for the [`transloadit:upload`](#transloaditupload) event when
+this or `waitForEncoding` is enabled.
+
+#### `importFromUploadURLs`
+
+Allow another plugin to upload files, and then import those files into the
+Transloadit Assembly (`boolean`, default: `false`).
+
+When enabling this option, Transloadit will _not_ configure the Tus plugin to
+upload to Transloadit. Instead, a separate upload plugin must be used. Once the
+upload completes, the Transloadit plugin adds the uploaded file to the Assembly.
+
+For example, to upload files to an S3 bucket and then transcode them:
+
+```js
+uppy.use(AwsS3, {
+	getUploadParameters(file) {
+		return {
+			/* upload parameters */
+		};
+	},
+});
+uppy.use(Transloadit, {
+	importFromUploadURLs: true,
+	assemblyOptions: {
+		params: {
+			auth: { key: 'YOUR_API_KEY' },
+			template_id: 'YOUR_TEMPLATE_ID',
+		},
+	},
+});
+```
+
+Tranloadit will download the files and expose them to your Template as
+`:original`, as if they were directly uploaded from the Uppy client.  
+:::note
+
+For this to work, the upload plugin must assign a publicly accessible
+`uploadURL` property to the uploaded file object. The Tus and S3 plugins both do
+this automatically, but you must configure your S3 bucket to have publicly
+readable objects. For the XHRUpload plugin, you may have to specify a custom
+`getResponseData` function.
+
+:::
+
+#### `alwaysRunAssembly`
+
+Always create and run an Assembly when `uppy.upload()` is called, even if no
+files were selected (`boolean`, default: `false`).
+
+This allows running Assemblies that do not receive files, but instead use a
+robot like [`/s3/import`](https://transloadit.com/docs/transcoding/#s3-import)
+to download the files from elsewhere, for example, for a bulk transcoding job.
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		// Shown while Assemblies are being created for an upload.
+		creatingAssembly: 'Preparing upload...',
+		// Shown if an Assembly could not be created.
+		creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
+		// Shown after uploads have succeeded, but when the Assembly is still executing.
+		// This only shows if `waitForMetadata` or `waitForEncoding` was enabled.
+		encoding: 'Encoding...',
+	},
+};
+```
+
+#### `clientName`
+
+Append a custom client name to the `Transloadit-Client` header field when
+creating an Assembly (`string`, default: `null`).
+
+The `Transloadit-Client` header includes by default information about the used
+SDK and is included in the Assembly Status under the `transloadit_client`
+property. By providing a value, such as `homepage-file-uploader`, you can
+identify the client and SDK that created a given Assembly.
+
+<details>
+  <summary>Deprecated options</summary>
+
+These options have been deprecated in favor of
+[`assemblyOptions`](#assemblyoptions), which we now recommend for all use cases.
+You can still use these options, but they will be removed in the next major
+version.
+
+#### `getAssemblyOptions`
+
+This function behaves the same as passing a function to
+[`assemblyOptions`](#assemblyoptions).
+
+#### `params`
+
+The Assembly parameters to use for the upload (`object`, default: `null`) See
+the Transloadit documentation on
+[Assembly Instructions](https://transloadit.com/docs/#14-assembly-instructions)
+for further information.
+
+The `auth.key` Assembly parameter is required. You can also use the `steps` or
+`template_id` options here as described in the Transloadit documentation.
+
+```js
+uppy.use(Transloadit, {
+	params: {
+		auth: { key: 'YOUR_TRANSLOADIT_KEY' },
+		steps: {
+			encode: {
+				robot: '/video/encode',
+				use: {
+					steps: [':original'],
+					fields: ['file_input_field2'],
+				},
+				preset: 'iphone',
+			},
+		},
+	},
+});
+```
+
+#### `signature`
+
+An optional signature for the Assembly parameters. See the Transloadit
+documentation on
+[Signature Authentication](https://transloadit.com/docs/#26-signature-authentication)
+for further information.
+
+If a `signature` is provided, `params` should be a JSON string instead of a
+JavaScript object, as otherwise the generated JSON in the browser may be
+different from the JSON string that was used to generate the signature.
+
+#### `fields`
+
+An object of form fields to send along to the Assembly. Keys are field names,
+and values are field values. See also the Transloadit documentation on
+[Form Fields In Instructions](https://transloadit.com/docs/#23-form-fields-in-instructions).
+
+```js
+uppy.use(Transloadit, {
+	// ...
+	fields: {
+		message: 'This is a form field',
+	},
+});
+```
+
+You can also pass an array of field names to send global or file metadata along
+to the Assembly. Global metadata is set using the
+[`meta` option](/docs/uppy/#meta) in the Uppy constructor, or using the
+[`setMeta` method](/docs/uppy/#uppy-setMeta-data). File metadata is set using
+the [`setFileMeta`](/docs/uppy/#uppy-setFileMeta-fileID-data) method. The
+[Form](/docs/form) plugin also sets global metadata based on the values of
+`<input />`s in the form, providing a handy way to use values from HTML form
+fields:
+
+```js
+uppy.use(Form, { target: 'form#upload-form', getMetaFromForm: true });
+uppy.use(Transloadit, {
+	fields: ['field_name', 'other_field_name'],
+	params: {
+		/* ... */
+	},
+});
+```
+
+Form fields can also be computed dynamically using custom logic, by using the
+[`getAssemblyOptions(file)`](/docs/transloadit/#getAssemblyOptions-file) option.
+
+</details>
+
+### Static exports
+
+#### `COMPANION_URL`
+
+The main endpoint for Transloadit’s hosted companions. You can use this constant
+in remote provider options, like so:
+
+```js
+import Dropbox from '@uppy/dropbox';
+import { COMPANION_URL } from '@uppy/transloadit';
+
+uppy.use(Dropbox, {
+	companionUrl: COMPANION_URL,
+});
+```
+
+When using `COMPANION_URL`, you should also configure
+[`companionAllowedHosts`](#companion_allowed_hosts).
+
+The value of this constant is `https://api2.transloadit.com/companion`. If you
+are using a custom [`service`](#service) option, you should also set a custom
+host option in your provider plugins, by taking a Transloadit API url and
+appending `/companion`:
+
+```js
+uppy.use(Dropbox, {
+	companionUrl: 'https://api2-us-east-1.transloadit.com/companion',
+});
+```
+
+#### `COMPANION_ALLOWED_HOSTS`
+
+A RegExp pattern matching Transloadit’s hosted companion endpoints. The pattern
+is used in remote provider `companionAllowedHosts` options, to make sure that
+third party authentication messages cannot be faked by an attacker’s page but
+can only originate from Transloadit’s servers.
+
+Use it whenever you use `companionUrl: COMPANION_URL`, like so:
+
+```js
+import Dropbox from '@uppy/dropbox';
+import { COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
+
+uppy.use(Dropbox, {
+	companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
+});
+```
+
+The value of this constant covers _all_ Transloadit’s Companion servers, so it
+does not need to be changed if you are using a custom [`service`](#service)
+option. But, if you are not using the Transloadit Companion servers at
+`*.transloadit.com`, make sure to set the `companionAllowedHosts` option to
+something that matches what you do use.
+
+### Events
+
+#### `transloadit:assembly-created`
+
+Fired when an Assembly is created.
+
+**Parameters**
+
+- `assembly` - The initial [Assembly Status][assembly-status].
+- `fileIDs` - The IDs of the files that will be uploaded to this Assembly.
+
+```js
+uppy.on('transloadit:assembly-created', (assembly, fileIDs) => {
+	console.group('Created', assembly.assembly_id, 'for files:');
+	for (const id of fileIDs) {
+		console.log(uppy.getFile(id).name);
+	}
+	console.groupEnd();
+});
+```
+
+#### `transloadit:upload`
+
+Fired when Transloadit has received an upload. Requires
+[`waitForMetadata`](#waitformetadata) to be set.
+
+**Parameters**
+
+- `file` - The Transloadit file object that was uploaded.
+- `assembly` - The [Assembly Status][assembly-status] of the Assembly to which
+  the file was uploaded.
+
+#### `transloadit:assembly-executing`
+
+Fired when Transloadit has received all uploads, and is executing the Assembly.
+
+**Parameters**
+
+- `assembly` - The
+  [Assembly Status](https://transloadit.com/docs/api/#assembly-status-response)
+  of the Assembly that is executing.
+
+#### `transloadit:result`
+
+Fired when a result came in from an Assembly. Requires
+[`waitForEncoding`](#waitforencoding) to be set.
+
+**Parameters**
+
+- `stepName` - The name of the Assembly step that generated this result.
+- `result` - The result object from Transloadit. This result object has one more
+  property, namely `localId`. This is the ID of the file in Uppy’s local state,
+  and can be used with `uppy.getFile(id)`.
+- `assembly` - The [Assembly Status][assembly-status] of the Assembly that
+  generated this result.
+
+```js
+uppy.on('transloadit:result', (stepName, result) => {
+	const file = uppy.getFile(result.localId);
+	document.body.appendChild(html`
+		<div>
+			<h2>From ${file.name}</h2>
+			<a href=${result.ssl_url}> View </a>
+		</div>
+	`);
+});
+```
+
+#### `transloadit:complete`
+
+Fired when an Assembly completed. Requires [`waitForEncoding`](#waitForEncoding)
+to be set.
+
+**Parameters**
+
+- `assembly` - The final [Assembly Status][assembly-status] of the completed
+  Assembly.
+
+```js
+uppy.on('transloadit:complete', (assembly) => {
+	// Could do something fun with this!
+	console.log(assembly.results);
+});
+```
+
+## Frequently Asked Questions
+
+### Accessing the assembly when an error occurred
+
+If an error occurs when an Assembly has already started, you can find the
+Assembly Status on the error object’s `assembly` property.
+
+```js
+uppy.on('error', (error) => {
+	if (error.assembly) {
+		console.log(`Assembly ID ${error.assembly.assembly_id} failed!`);
+		console.log(error.assembly);
+	}
+});
+```
+
+### Assembly behavior when Uppy is closed
+
+When integrating `@uppy/transloadit` with `@uppy/dashboard`, closing the
+dashboard will result in continuing assemblies on the server. When the user
+manually cancels the upload any running assemblies will be cancelled.
+
+[assembly-status]: https://transloadit.com/docs/api/#assembly-status-response
+[template-credentials]:
+	https://transloadit.com/docs/#how-to-create-template-credentials
+[transloadit-services]: https://transloadit.com/services/
+[transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/

+ 308 - 0
docs/uploader/tus.mdx

@@ -0,0 +1,308 @@
+---
+sidebar_position: 2
+slug: /tus
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Tus
+
+The `@uppy/tus` plugin brings resumable file uploading with [Tus](http://tus.io)
+to Uppy by wrapping the [`tus-js-client`][].
+
+## When should I use it?
+
+:::tip
+
+Not sure which uploader is best for you? Read
+“[Choosing the uploader you need](/docs/guides/choosing-uploader)”.
+
+:::
+
+[Tus][tus] is an open protocol for resumable uploads built on HTTP. This means
+accidentally closing your tab or losing connection let’s you continue, for
+instance, your 10GB upload instead of starting all over.
+
+Tus supports any language, any platform, and any network. It requires a client
+and server integration to work. You can checkout the client and server
+[implementations][] to find the server in your preferred language. You can store
+files on the Tus server itself, but you can also use service integrations (such
+as S3) to store files externally. If you don’t want to host your own server, see
+“[Are there hosted Tus servers?](#are-there-hosted-tus-servers)”.
+
+If you want reliable, resumable uploads: use `@uppy/tus` to connect to your Tus
+server in a few lines of code.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/tus
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/tus
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Tus } from "{{UPPY_JS_URL}}"
+        new Uppy().use(Tus, { endpoint: 'https://tusd.tusdemo.net/files' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+A quick overview of the complete API.
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Tus from '@uppy/tus';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' });
+```
+
+## API
+
+### Options
+
+:::info
+
+All options are passed to `tus-js-client` and we document the ones here that are
+required, added, or changed. This means you can also pass functions like
+[`onAfterResponse`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#onafterresponse).
+
+We recommended taking a look at the
+[API reference](https://github.com/tus/tus-js-client/blob/master/docs/api.md)
+from `tus-js-client` to know what is supported.
+
+:::
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Tus'`).
+
+#### `endpoint`
+
+URL of the tus server (`string`, default: `null`).
+
+#### `headers`
+
+An object or function returning an object with HTTP headers to send along
+requests (`object | function`, default: `null`).
+
+Keys are header names, values are header values.
+
+```js
+const headers = {
+	authorization: `Bearer ${window.getCurrentUserToken()}`,
+};
+```
+
+Header values can also be derived from file data by providing a function. The
+function receives an [Uppy file][] and must return an object where the keys are
+header names, and values are header values.
+
+```js
+const headers = (file) => {
+	return {
+		authorization: `Bearer ${window.getCurrentUserToken()}`,
+		expires: file.meta.expires,
+	};
+};
+```
+
+#### `chunkSize`
+
+A number indicating the maximum size of a `PATCH` request body in bytes
+(`number`, default: `Infinity`). Note that this option only affects local
+browser uploads. If you need a max chunk size for remote (Companion) uploads,
+you must set the `chunkSize` Companion option as well.
+
+:::caution
+
+Do not set this value unless you are forced to. The two valid reasons are
+described in the
+[`tus-js-client` docs](https://github.com/tus/tus-js-client/blob/master/docs/api.md#chunksize).
+
+:::
+
+#### `withCredentials`
+
+Configure the requests to send Cookies using the
+[`xhr.withCredentials`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
+property (`boolean`, default: `false`).
+
+The remote server must accept CORS and credentials.
+
+#### `retryDelays`
+
+When uploading a chunk fails, automatically try again after the defined
+millisecond intervals (`Array<number>`, default: `[0, 1000, 3000, 5000]`).
+
+By default, we first retry instantly; if that fails, we retry after 1 second; if
+that fails, we retry after 3 seconds, etc.
+
+Set to `null` to disable automatic retries, and fail instantly if any chunk
+fails to upload.
+
+#### `onBeforeRequest(req, file)`
+
+Behaves like the
+[`onBeforeRequest`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#onbeforerequest)
+function from `tus-js-client` but with the added `file` argument.
+
+#### `onShouldRetry: (err, retryAttempt, options, next)`
+
+When an upload fails `onShouldRetry` is called with the error and the default
+retry logic as the last argument (`function`).
+
+The default retry logic is an
+[exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff)
+algorithm triggered on HTTP 429 (Too Many Requests) errors. Meaning if your
+server (or proxy) returns HTTP 429 because it’s being overloaded, @uppy/tus will
+find the ideal sweet spot to keep uploading without overloading.
+
+If you want to extend this functionality, for instance to retry on unauthorized
+requests (to retrieve a new authentication token):
+
+```js
+import Uppy from '@uppy/core';
+import Tus from '@uppy/tus';
+new Uppy().use(Tus, {
+	endpoint: '',
+	async onBeforeRequest(req) {
+		const token = await getAuthToken();
+		req.setHeader('Authorization', `Bearer ${token}`);
+	},
+	onShouldRetry(err, retryAttempt, options, next) {
+		if (err?.originalResponse?.getStatus() === 401) {
+			return true;
+		}
+		return next(err);
+	},
+	async onAfterResponse(req, res) {
+		if (res.getStatus() === 401) {
+			await refreshAuthToken();
+		}
+	},
+});
+```
+
+#### `allowedMetaFields`
+
+Pass an array of field names to limit the metadata fields that will be added to
+uploads as
+[Tus Metadata](https://tus.io/protocols/resumable-upload.html#upload-metadata)
+(`Array`, default: `null`).
+
+- Set this to `['name']` to only send the `name` field.
+- Set this to `null` (the default) to send _all_ metadata fields.
+- Set this to an empty array `[]` to not send any fields.
+
+#### `limit`
+
+Limit the amount of uploads going on at the same time (`number`, default: `20`).
+
+Setting this to `0` means no limit on concurrent uploads (not recommended).
+
+## Frequently Asked Questions
+
+:::info
+
+The Tus website has extensive [FAQ section](https://tus.io/faq.html), we
+recommend taking a look there as well if something is unclear.
+
+:::
+
+### How is file meta data stored?
+
+Tus uses unique identifiers for the file names to prevent naming collisions. To
+still keep the meta data in place, Tus also uploads an extra `.info` file with
+the original file name and other meta data:
+
+```json
+{
+	"ID": "00007a99d16d4eeb5a3e3c080b6f69da+JHZavdqPSK4VMtarg2yYcNiP8t_kDjN51lBYMJdEyr_wqEotVl8ZBRBSTnWKWenZBwHvbLNz5tQXYp2N7Vdol.04ysQAuw__suTJ4IsCljj0rjyWA6LvV4IwF5P2oom2",
+	"Size": 1679852,
+	"SizeIsDeferred": false,
+	"Offset": 0,
+	"MetaData": {
+		"filename": "cat.jpg",
+		"filetype": "image/jpeg"
+	},
+	"IsPartial": false,
+	"IsFinal": false,
+	"PartialUploads": null,
+	"Storage": {
+		"Bucket": "your-bucket",
+		"Key": "some-key",
+		"Type": "s3store"
+	}
+}
+```
+
+### How do I change files before sending them?
+
+If you want to change the file names, you want to do that in
+[`onBeforeFileAdded`](/docs/uppy#onbeforefileaddedfile-files).
+
+If you want to send extra headers with the request, use [`headers`](#headers) or
+[`onBeforeRequest`](#onbeforerequestreq-file).
+
+### How do I change (or move) files after sending them?
+
+If you want to preserve files names, extract meta data, or move files to a
+different place you generally can with hooks or events. It depends on the Tus
+server you use how it’s done exactly. [`tusd`][], for instance, exposes
+[hooks](https://github.com/tus/tusd/blob/master/docs/hooks.md) and
+[`tus-node-server`](https://github.com/tus/tus-node-server) has
+[events](https://github.com/tus/tus-node-server#events).
+
+### Which server do you recommend?
+
+[Transloadit](https://transloadit.com) runs [`tusd`][] in production, where it
+serves millions of requests globally. So we recommend `tusd` as battle-tested
+from our side, but other companies have had success with other
+[implementations][] so it depends on your needs.
+
+### Are there hosted Tus servers?
+
+All [Transloadit plans](https://transloadit.com/pricing) come with a hosted
+[`tusd`][] server. You don’t have to do anything to leverage it, using
+[`@uppy/transloadit`](/docs/transloadit) automatically uses Tus under the hood.
+
+### Why Tus instead of directly uploading to AWS S3?
+
+First: reliable, resumable uploads. This means accidentally closing your tab or
+losing connection let’s you continue, for instance, your 10GB upload instead of
+starting all over.
+
+Tus is also efficient with lots of files (such as 8K) and large files. Uploading
+to AWS S3 directly from the client also introduces quite a bit of overhead, as
+more requests are needed for the flow to work.
+
+[`tus-js-client`]: https://github.com/tus/tus-js-client
+[uppy file]: /docs/uppy#working-with-uppy-files
+[tus]: https://tus.io/
+[`tusd`]: https://github.com/tus/tusd
+[implementations]: https://tus.io/implementations.html

+ 399 - 0
docs/uploader/xhr.mdx

@@ -0,0 +1,399 @@
+---
+sidebar_position: 5
+slug: /xhr-upload
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# XHR
+
+The `@uppy/xhr-upload` plugin is for regular uploads to a HTTP server.
+
+## When should I use it?
+
+:::tip
+
+Not sure which uploader is best for you? Read
+“[Choosing the uploader you need](/docs/guides/choosing-uploader)”.
+
+:::
+
+When you have an existing HTTP server and you don’t need Transloadit services or
+want to run a [tus][] server. Note that it’s still possible to use [tus][]
+without running an extra server by integrating tus into your existing one. For
+instance, if you have a Node.js server (or server-side framework like Next.js)
+you could integrate [tus-node-server][].
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/xhr-upload
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/xhr-upload
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, XHRUpload } from "{{UPPY_JS_URL}}"
+        new Uppy().use(XHRUpload, { endpoint: 'https://tusd.tusdemo.net/files' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+A quick overview of the complete API.
+
+```js {10} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import XHR from '@uppy/xhr-upload';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: 'body' })
+	.use(XHR, { endpoint: 'https://your-domain.com/upload' });
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'XHRUpload'`).
+
+#### `endpoint`
+
+URL of the HTTP server (`string`, default: `null`).
+
+#### `method`
+
+Configures which HTTP method to use for the upload (`string`, default:
+`'post'`).
+
+#### `formData`
+
+Configures whether to use a multipart form upload, using [FormData][]
+(`boolean`, default: `true`).
+
+This works similarly to using a `<form>` element with an `<input type="file">`
+for uploads. When set to `true`, file metadata is also sent to the endpoint as
+separate form fields. When set to `false`, only the file contents are sent.
+
+#### `fieldName`
+
+When [`formData`](#formData-true) is set to true, this is used as the form field
+name for the file to be uploaded.
+
+It defaults to `'files[]'` if `bundle` option is set to `true`, otherwise it
+defaults to `'file'`.
+
+#### `allowedMetaFields`
+
+Pass an array of field names to limit the metadata fields that will be added to
+upload.
+
+- Set this to an empty array `[]` to not send any fields.
+- Set this to `['name']` to only send the `name` field.
+- Set this to `null` (the default) to send _all_ metadata fields.
+
+If the [`formData`](#formData-true) option is set to false, `metaFields` is
+ignored.
+
+#### `headers`
+
+An object containing HTTP headers to use for the upload request. Keys are header
+names, values are header values.
+
+```js
+const headers = {
+	authorization: `Bearer ${window.getCurrentUserToken()}`,
+};
+```
+
+Header values can also be derived from file data by providing a function. The
+function receives an [Uppy file][] and must return an object where the keys are
+header names, and values are header values.
+
+```js
+const headers = (file) => {
+	return {
+		authorization: `Bearer ${window.getCurrentUserToken()}`,
+		expires: file.meta.expires,
+	};
+};
+```
+
+:::note
+
+The function syntax is not available when [`bundle`](#bundle) is set to `true`.
+
+:::
+
+#### `bundle`
+
+Send all files in a single multipart request (`boolean`, default: `false`).
+
+All files will be appended to the provided `fieldName` field in the request.
+
+:::caution
+
+When `bundle` is set to `true`:
+
+- [`formData`](#formData-true) must also be set to `true`.
+- Uppy won’t be able to bundle remote files (such as Google Drive) and will
+  throw an error in this case.
+- Only [global uppy metadata](/docs/uppy/#meta) is sent to the endpoint.
+  Individual per-file metadata is ignored.
+
+:::
+
+To upload files on different fields, use
+[`uppy.setFileState()`](/docs/uppy#uppy-setFileState-fileID-state) to set the
+`xhrUpload.fieldName` property on the file:
+
+```js
+uppy.setFileState(fileID, {
+	xhrUpload: { fieldName: 'pic0' },
+});
+```
+
+#### `validateStatus`
+
+Check if the response was successful (`function`, default:
+`(status, responseText, response) => boolean`).
+
+- By default, responses with a 2xx HTTP status code are considered successful.
+- When `true`, [`getResponseData()`](#getResponseData-responseText-response)
+  will be called and the upload will be marked as successful.
+- When `false`, both
+  [`getResponseData()`](#getResponseData-responseText-response) and
+  [`getResponseError()`](#getResponseError-responseText-response) will be called
+  and the upload will be marked as unsuccessful.
+
+##### Parameters
+
+- The `statusCode` is the numeric HTTP status code returned by the endpoint.
+- The `responseText` is the XHR endpoint response as a string.
+- `response` is the [XMLHttpRequest][] object.
+
+:::note
+
+This option is only used for **local** uploads. Uploads from remote providers
+like Google Drive or Instagram do not support this and will always use the
+default.
+
+:::
+
+#### `getResponseData`
+
+Extract the response data from the successful upload (`function`, default:
+`(responseText, response) => void`).
+
+- `responseText` is the XHR endpoint response as a string.
+- `response` is the [XMLHttpRequest][] object.
+
+JSON is handled automatically, so you should only use this if the endpoint
+responds with a different format. For example, an endpoint that responds with an
+XML document:
+
+```js
+function getResponseData(responseText, response) {
+	const parser = new DOMParser();
+	const xmlDoc = parser.parseFromString(responseText, 'text/xml');
+	return {
+		url: xmlDoc.querySelector('Location').textContent,
+	};
+}
+```
+
+:::note
+
+This response data will be available on the file’s `.response` property and will
+be emitted in the [`upload-success`][uppy.upload-success] event.
+
+:::
+
+:::note
+
+When uploading files from remote providers such as Dropbox or Instagram,
+Companion sends upload response data to the client. This is made available in
+the `getResponseData()` function as well. The `response` object from Companion
+has some properties named after their [XMLHttpRequest][] counterparts.
+
+:::
+
+#### `getResponseError`
+
+Extract the error from the failed upload (`function`, default:
+`(responseText, response) => void`).
+
+For example, if the endpoint responds with a JSON object containing a
+`{ message }` property, this would show that message to the user:
+
+```js
+function getResponseError(responseText, response) {
+	return new Error(JSON.parse(responseText).message);
+}
+```
+
+#### `responseUrlFieldName`
+
+The field name containing the location of the uploaded file (`string`, default:
+`'url'`).
+
+This is returned by [`getResponseData()`](#getResponseData).
+
+#### `timeout: 30 * 1000`
+
+Abort the connection if no upload progress events have been received for this
+milliseconds amount (`number`, default: `30_000`).
+
+Note that unlike the [`XMLHttpRequest.timeout`][xhr.timeout] property, this is a
+timer between progress events: the total upload can take longer than this value.
+Set to `0` to disable this check.
+
+#### `limit`
+
+The maximum amount of files to upload in parallel (`number`, default: `5`).
+
+#### `responseType`
+
+The response type expected from the server, determining how the `xhr.response`
+property should be filled (`string`, default: `'text'`).
+
+The `xhr.response` property can be accessed in a custom
+[`getResponseData()`](#getResponseData-responseText-response) callback. This
+option sets the [`XMLHttpRequest.responseType`][xhr.responsetype] property. Only
+`''`, `'text'`, `'arraybuffer'`, `'blob'` and `'document'` are widely supported
+by browsers, so it’s recommended to use one of those.
+
+#### `withCredentials`
+
+Indicates whether cross-site Access-Control requests should be made using
+credentials (`boolean`, default: `false`).
+
+#### `locale: {}`
+
+```js
+export default {
+	strings: {
+		// Shown in the Informer if an upload is being canceled because it stalled for too long.
+		timedOut: 'Upload stalled for %{seconds} seconds, aborting.',
+	},
+};
+```
+
+## Frequently Asked Questions
+
+### How to send along meta data with the upload?
+
+When using XHRUpload with [`formData: true`](#formData-true), file metadata is
+sent along with each upload request. You can set metadata for a file using
+[`uppy.setFileMeta(fileID, data)`](/docs/uppy#uppy-setFileMeta-fileID-data), or
+for all files simultaneously using
+[`uppy.setMeta(data)`](/docs/uppy#uppy-setMeta-data).
+
+It may be useful to set metadata depending on some file properties, such as the
+size. You can use the [`file-added`](/docs/uppy/#file-added) event and the
+[`uppy.setFileMeta(fileID, data)`](/docs/uppy#uppy-setFileMeta-fileID-data)
+method to do this:
+
+```js
+uppy.on('file-added', (file) => {
+	uppy.setFileMeta(file.id, {
+		size: file.size,
+	});
+});
+```
+
+Now, a form field named `size` will be sent along to the
+[`endpoint`](#endpoint-39-39) once the upload starts.
+
+By default, all metadata is sent, including Uppy’s default `name` and `type`
+metadata. If you do not want the `name` and `type` metadata properties to be
+sent to your upload endpoint, you can use the [`metaFields`](#metaFields-null)
+option to restrict the field names that should be sent.
+
+```js
+uppy.use(XHRUpload, {
+	// Only send our own `size` metadata field.
+	allowedMetaFields: ['size'],
+});
+```
+
+### How to upload to a PHP server?
+
+The XHRUpload plugin works similarly to a `<form>` upload. You can use the
+`$_FILES` variable on the server to work with uploaded files. See the PHP
+documentation on [Handling file uploads][php.file-upload].
+
+The default form field for file uploads is `files[]`, which means you have to
+access the `$_FILES` array as described in [Uploading many files][php.multiple]:
+
+```php
+<?php
+// upload.php
+$files = $_FILES['files'];
+$file_path = $files['tmp_name'][0]; // temporary upload path of the first file
+$file_name = $_POST['name']; // desired name of the file
+move_uploaded_file($file_path, './img/' . basename($file_name)); // save the file in `img/`
+```
+
+Note how we are using `$_POST['name']` instead of `$my_file['name']`.
+`$my_file['name']` has the original name of the file on the user’s device.
+`$_POST['name']` has the `name` metadata value for the uploaded file, which can
+be edited by the user using the [Dashboard](/docs/dashboard).
+
+Set a custom `fieldName` to make working with the `$_FILES` array a bit less
+convoluted:
+
+```js
+// app.js
+uppy.use(XHRUpload, {
+	endpoint: '/upload.php',
+	fieldName: 'my_file',
+});
+```
+
+```php
+<?php
+// upload.php
+$my_file = $_FILES['my_file'];
+$file_path = $my_file['tmp_name']; // temporary upload path of the file
+$file_name = $_POST['name']; // desired name of the file
+move_uploaded_file($file_path, $_SERVER['DOCUMENT_ROOT'] . '/img/' . basename($file_name)); // save the file at `img/FILE_NAME`
+```
+
+[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
+[xmlhttprequest]:
+	https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
+[xhr.timeout]:
+	https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
+[xhr.responsetype]:
+	https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
+[uppy.upload-success]: /docs/uppy/#upload-success
+[uppy file]: /docs/uppy#working-with-uppy-files
+[php.file-upload]: https://secure.php.net/manual/en/features.file-upload.php
+[php.multiple]:
+	https://secure.php.net/manual/en/features.file-upload.multiple.php
+[tus-node-server]: https://github.com/tus/tus-node-server
+[tus]: https://tus.io/

+ 1628 - 0
docs/uppy-core.mdx

@@ -0,0 +1,1628 @@
+---
+sidebar_position: 3
+slug: /uppy
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Uppy core
+
+Uppy can be an uploader and an interface with a lot of features. Features can be
+added incrementally with plugins, but Uppy can be as bare bones as you want it
+to be. So we build Uppy’s heart, `@uppy/core`, as a standalone orchestrator. It
+acts as a state manager, event emitter, and restrictions handler.
+
+## When should I use it?
+
+`@uppy/core` is the fundament of the Uppy ecosystem, the orchestrator for all
+added plugins. No matter the uploading experience you’re looking for, it all
+starts with installing this plugin.
+
+You can use `@uppy/core` and
+[build your own UI](/docs/guides/building-your-own-ui-with-uppy) or go for the
+[Dashboard](/docs/dashboard) integration. For an uploading plugin, you can refer
+to [choosing the uploader you need](/docs/guides/choosing-uploader).
+
+If you want to see how it all comes together, checkout the
+[examples](/examples).
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/core
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/core
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+`@uppy/core` has four exports: `Uppy`, `UIPlugin`, `BasePlugin`, and
+`debugLogger`. The default export is the `Uppy` class.
+
+### Working with Uppy files
+
+Uppy keeps files in state with the [`File`][] browser API, but it’s wrapped in
+an `Object` to be able to add more data to it, which we call an _Uppy file_. All
+these properties can be useful for plugins and side-effects (such as
+[events](#events)).
+
+Mutating these properties should be done through [methods](#methods).
+
+<details>
+  <summary>Uppy file properties</summary>
+
+#### `file.source`
+
+Name of the plugin that was responsible for adding this file. Typically a remote
+provider plugin like `'GoogleDrive'` or a UI plugin like `'DragDrop'`.
+
+#### `file.id`
+
+Unique ID for the file.
+
+#### `file.name`
+
+The name of the file.
+
+#### `file.meta`
+
+Object containing standard as well as user-defined metadata for each file. Any
+custom file metadata should be JSON-serializable. The following standard
+metadata will be stored on all file objects, but plugins may add more metadata.
+
+- `file.meta.name`
+  - Same as `file.name`.
+- `file.meta.type`
+  - Same as `file.type`.
+- `file.meta.relativePath`
+  - For any local folder that was drag-dropped or opened in Uppy, the files
+    inside the folder will have the `relativePath` metadata field set to their
+    path, relative to the folder. `relativePath` begins with the folder’s name
+    and ends with the file’s name. If opening or drag-dropping a file instead of
+    a folder, `relativePath` will be `null`. The same behaviour exists for
+    remote (provider) files, but the path will instead be relative to the user’s
+    selection (checkboxes). No leading or trailing slashes.
+  - **Local file example:** When drag-dropping a local folder `folder1` which
+    has a folder inside of it named `folder2` which has a file named `file`
+    inside of it, the `relativePath` meta field of the file will be
+    `folder1/folder2/file`. However if drag-dropping or opening `file` directly,
+    `relativePath` will be `null`.
+  - **Remote file example:** Suppose we have a remote provider folder structure
+    such as `/folder1/folder2/file`. Then, if the user checks the checkbox next
+    to `folder1`, `file`’s `relativePath` will be `"folder1/folder2/file"`.
+    However if the user first navigates into `folder1`, and only then checks the
+    checkbox next to `folder2`, `relativePath` will be `"folder2/file"`.
+- `file.meta.absolutePath`
+  - The `absolutePath` meta field will only be set for remote files. Regardless
+    of user selection, it will always be the path relative to the root of the
+    provider’s list of files, as presented to the user. `absolutePath` always
+    begins with a `/` and will always end with the file’s name. To clarify: The
+    difference between `absolutePath` and `relativePath` is that `absolutePath`
+    only exists for remote files, and always has the full path to the file,
+    while `relativePath` is the file’s path _relative to the user’s selected
+    folder_.
+
+#### `file.type`
+
+MIME type of the file. This may actually be guessed if a file type was not
+provided by the user’s browser, so this is a best-effort value and not
+guaranteed to be correct.
+
+#### `file.data`
+
+For local files, this is the actual [`File`][] or [`Blob`][] object representing
+the file contents.
+
+For files that are imported from remote providers, the file data is not
+available in the browser.
+
+[`file`]: https://developer.mozilla.org/en-US/docs/Web/API/File
+[`blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
+
+#### `file.progress`
+
+An object with upload progress data.
+
+**Properties**
+
+- `bytesUploaded` - Number of bytes uploaded so far.
+- `bytesTotal` - Number of bytes that must be uploaded in total.
+- `uploadStarted` - Null if the upload has not started yet. Once started, this
+  property stores a UNIX timestamp. Note that this is only set _after_
+  preprocessing.
+- `uploadComplete` - Boolean indicating if the upload has completed. Note this
+  does _not_ mean that postprocessing has completed, too.
+- `percentage` - Integer percentage between 0 and 100.
+
+#### `file.size`
+
+Size in bytes of the file.
+
+#### `file.isRemote`
+
+Boolean: is this file imported from a remote provider?
+
+#### `file.remote`
+
+Grab bag of data for remote providers. Generally not interesting for end users.
+
+#### `file.preview`
+
+An optional URL to a visual thumbnail for the file.
+
+#### `file.uploadURL`
+
+When an upload is completed, this may contain a URL to the uploaded file.
+Depending on server configuration it may not be accessible or correct.
+
+</details>
+
+## `new Uppy(options?)`
+
+```js
+import Uppy from '@uppy/core';
+
+const uppy = new Uppy();
+```
+
+### Options
+
+#### `id`
+
+A site-wide unique ID for the instance (`string`, default: `uppy`).
+
+:::note
+
+If several Uppy instances are being used, for instance, on two different pages,
+an `id` should be specified. This allows Uppy to store information in
+`localStorage` without colliding with other Uppy instances.
+
+This ID should be persistent across page reloads and navigation—it shouldn’t be
+a random number that is different every time Uppy is loaded.
+
+:::
+
+#### `autoProceed`
+
+Upload as soon as files are added (`boolean`, default: `false`).
+
+By default Uppy will wait for an upload button to be pressed in the UI, or the
+`.upload()` method to be called before starting an upload. Setting this to
+`true` will start uploading automatically after the first file is selected
+
+#### `allowMultipleUploadBatches`
+
+Whether to allow several upload batches (`boolean`, default: `true`).
+
+This means several calls to `.upload()`, or a user adding more files after
+already uploading some. An upload batch is made up of the files that were added
+since the earlier `.upload()` call.
+
+With this option set to `true`, users can upload some files, and then add _more_
+files and upload those as well. A model use case for this is uploading images to
+a gallery or adding attachments to an email.
+
+With this option set to `false`, users can upload some files, and you can listen
+for the [`'complete'`](#complete) event to continue to the next step in your
+app’s upload flow. A typical use case for this is uploading a new profile
+picture. If you are integrating with an existing HTML form, this option gives
+the closest behaviour to a bare `<input type="file">`.
+
+#### `debug`
+
+Whether to send debugging and warning logs (`boolean`, default: `false`).
+
+Setting this to `true` sets the [`logger`](#logger) to
+[`debugLogger`](#debuglogger).
+
+#### `logger`
+
+Logger used for [`uppy.log`](#logmessage-type) (`Object`, default:
+`justErrorsLogger`).
+
+By providing your own `logger`, you can send the debug information to a server,
+choose to log errors only, etc.
+
+:::note
+
+Set `logger` to [`debugLogger`](#debuglogger) to get debug info output to the
+browser console:
+
+:::
+
+:::note
+
+You can also provide your own logger object: it should expose `debug`, `warn`
+and `error` methods, as shown in the examples below.
+
+Here’s an example of a `logger` that does nothing:
+
+```js
+const nullLogger = {
+	debug: (...args) => {},
+	warn: (...args) => {},
+	error: (...args) => {},
+};
+```
+
+:::
+
+#### `restrictions`
+
+Conditions for restricting an upload (`Object`, default: `{}`).
+
+| Property             | Value           | Description                                                                                                                      |
+| -------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| `maxFileSize`        | `number`        | maximum file size in bytes for each individual file                                                                              |
+| `minFileSize`        | `number`        | minimum file size in bytes for each individual file                                                                              |
+| `maxTotalFileSize`   | `number`        | maximum file size in bytes for all the files that can be selected for upload                                                     |
+| `maxNumberOfFiles`   | `number`        | total number of files that can be selected                                                                                       |
+| `minNumberOfFiles`   | `number`        | minimum number of files that must be selected before the upload                                                                  |
+| `allowedFileTypes`   | `Array`         | wildcards `image/*`, or exact mime types `image/jpeg`, or file extensions `.jpg`: `['image/*', '.jpg', '.jpeg', '.png', '.gif']` |
+| `requiredMetaFields` | `Array<string>` | make keys from the `meta` object in every file required before uploading                                                         |
+
+:::note
+
+`maxNumberOfFiles` also affects the number of files a user is able to select via
+the system file dialog in UI plugins like `DragDrop`, `FileInput` and
+`Dashboard`. When set to `1`, they will only be able to select a single file.
+When `null` or another number is provided, they will be able to select several
+files.
+
+:::
+
+:::note
+
+`allowedFileTypes` gets passed to the file system dialog via the
+[`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Limiting_accepted_file_types)
+accept attribute, so only types supported by the browser will work.
+
+:::
+
+:::tip
+
+If you’d like to force a certain meta field data to be entered before the
+upload, you can
+[do so using `onBeforeUpload`](https://github.com/transloadit/uppy/issues/1703#issuecomment-507202561).
+
+:::
+
+:::tip
+
+If you need to restrict `allowedFileTypes` to a file extension with double dots,
+like `.nii.gz`, you can do so by
+[setting `allowedFileTypes` to the last part of the extension, `allowedFileTypes: ['.gz']`, and then using `onBeforeFileAdded` to filter for `.nii.gz`](https://github.com/transloadit/uppy/issues/1822#issuecomment-526801208).
+
+:::
+
+#### `meta`
+
+Key/value pairs to add to each file’s `metadata` (`Object`, default: `{}`).
+
+:::note
+
+Metadata from each file is then attached to uploads in the [Tus](/docs/tus) and
+[XHR](/docs/xhr-upload) plugins.
+
+:::
+
+:::info
+
+Two methods also exist for updating `metadata`: [`setMeta`](#setmetadata) and
+[`setFileMeta`](#setfilemetafileid-data).
+
+:::
+
+:::info
+
+Metadata can also be added from a `<form>` element on your page, through the
+[Form](#) plugin or through the UI if you are using Dashboard with the
+[`metaFields`](/docs/dashboard#metafields) option.
+
+:::
+
+<i id="onBeforeFileAdded" />
+
+#### `onBeforeFileAdded(file, files)`
+
+A function called before a file is added to Uppy (`Function`, default:
+`(files, file) => !Object.hasOwn(files, file.id)`).
+
+Use this function to run any number of custom checks on the selected file, or
+manipulate it, for instance, by optimizing a file name. You can also allow
+duplicate files with this.
+
+You can return `true` to keep the file as is, `false` to remove the file, or
+return a modified file.
+
+:::caution
+
+This method is intended for quick synchronous checks and modifications only. If
+you need to do an async API call, or heavy work on a file (like compression or
+encryption), you should use a [custom plugin](/docs/guides/building-plugins)
+instead.
+
+:::
+
+:::info
+
+No notification will be shown to the user about a file not passing validation by
+default. We recommend showing a message using
+[`uppy.info()`](#infomessage-type-duration) and logging to console for debugging
+purposes via [`uppy.log()`](#logmessage-type).
+
+:::
+
+<details>
+  <summary>Filter, change, and abort example</summary>
+
+Allow all files, also duplicate files. This will replace the file if it has not
+been uploaded. If you upload a duplicate file again it depends on your upload
+plugin and backend how it is handled.
+
+```js
+const uppy = new Uppy({
+  // ...
+  onBeforeFileAdded: () => true,
+```
+
+Keep only files under a condition:
+
+```js
+const uppy = new Uppy({
+  // ...
+  onBeforeFileAdded: (currentFile, files) => {
+    if (currentFile.name === 'forest-IMG_0616.jpg') {
+      return true
+    }
+    return false
+  },
+```
+
+Change all file names:
+
+```js
+const uppy = new Uppy({
+  // ...
+  onBeforeFileAdded: (currentFile, files) => {
+    const modifiedFile = {
+      ...currentFile,
+      name: `${currentFile.name}__${Date.now()}`,
+    }
+    return modifiedFile
+  },
+```
+
+Abort a file:
+
+```js
+const uppy = new Uppy({
+	// ...
+	onBeforeFileAdded: (currentFile, files) => {
+		if (!currentFile.type) {
+			// log to console
+			uppy.log(`Skipping file because it has no type`);
+			// show error message to the user
+			uppy.info(`Skipping file because it has no type`, 'error', 500);
+			return false;
+		}
+	},
+});
+```
+
+</details>
+
+#### `onBeforeUpload(files)`
+
+A function called before when upload is initiated (`Function`, default:
+`(files) => files`).
+
+Use this to check if all files or their total number match your requirements, or
+manipulate all the files at once before upload.
+
+You can return `true` to continue the upload, `false` to cancel it, or return
+modified files.
+
+:::caution
+
+This method is intended for quick synchronous checks and modifications only. If
+you need to do an async API call, or heavy work on a file (like compression or
+encryption), you should use a [custom plugin](/docs/guides/building-plugins)
+instead.
+
+:::
+
+:::info
+
+No notification will be shown to the user about a file not passing validation by
+default. We recommend showing a message using
+[`uppy.info()`](#infomessage-type-duration) and logging to console for debugging
+purposes via [`uppy.log()`](#logmessage-type).
+
+:::
+
+<details>
+  <summary>Change and abort example</summary>
+
+Change all file names:
+
+```js
+const uppy = new Uppy({
+	// ...
+	onBeforeUpload: (files) => {
+		// We’ll be careful to return a new object, not mutating the original `files`
+		const updatedFiles = {};
+		Object.keys(files).forEach((fileID) => {
+			updatedFiles[fileID] = {
+				...files[fileID],
+				name: `${myCustomPrefix}__${files[fileID].name}`,
+			};
+		});
+		return updatedFiles;
+	},
+});
+```
+
+Abort an upload:
+
+```js
+const uppy = new Uppy({
+	// ...
+	onBeforeUpload: (files) => {
+		if (Object.keys(files).length < 2) {
+			// log to console
+			uppy.log(
+				`Aborting upload because only ${
+					Object.keys(files).length
+				} files were selected`,
+			);
+			// show error message to the user
+			uppy.info(`You have to select at least 2 files`, 'error', 500);
+			return false;
+		}
+		return true;
+	},
+});
+```
+
+</details>
+
+#### `locale`
+
+You can override locale strings by passing the `strings` object with the keys
+you want to override.
+
+:::note
+
+Array indexed objects are used for pluralisation.
+
+:::
+
+:::info
+
+If you want a different language it’s better to use [locales](/docs/locales).
+
+:::
+
+```js
+module.exports = {
+	strings: {
+		addBulkFilesFailed: {
+			0: 'Failed to add %{smart_count} file due to an internal error',
+			1: 'Failed to add %{smart_count} files due to internal errors',
+		},
+		youCanOnlyUploadX: {
+			0: 'You can only upload %{smart_count} file',
+			1: 'You can only upload %{smart_count} files',
+		},
+		youHaveToAtLeastSelectX: {
+			0: 'You have to select at least %{smart_count} file',
+			1: 'You have to select at least %{smart_count} files',
+		},
+		exceedsSize: '%{file} exceeds maximum allowed size of %{size}',
+		missingRequiredMetaField: 'Missing required meta fields',
+		missingRequiredMetaFieldOnFile:
+			'Missing required meta fields in %{fileName}',
+		inferiorSize: 'This file is smaller than the allowed size of %{size}',
+		youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
+		noMoreFilesAllowed: 'Cannot add more files',
+		noDuplicates:
+			"Cannot add the duplicate file '%{fileName}', it already exists",
+		companionError: 'Connection with Companion failed',
+		authAborted: 'Authentication aborted',
+		companionUnauthorizeHint:
+			'To unauthorize to your %{provider} account, please go to %{url}',
+		failedToUpload: 'Failed to upload %{file}',
+		noInternetConnection: 'No Internet connection',
+		connectedToInternet: 'Connected to the Internet',
+		// Strings for remote providers
+		noFilesFound: 'You have no files or folders here',
+		selectX: {
+			0: 'Select %{smart_count}',
+			1: 'Select %{smart_count}',
+		},
+		allFilesFromFolderNamed: 'All files from folder %{name}',
+		openFolderNamed: 'Open folder %{name}',
+		cancel: 'Cancel',
+		logOut: 'Log out',
+		filter: 'Filter',
+		resetFilter: 'Reset filter',
+		loading: 'Loading...',
+		authenticateWithTitle:
+			'Please authenticate with %{pluginName} to select files',
+		authenticateWith: 'Connect to %{pluginName}',
+		signInWithGoogle: 'Sign in with Google',
+		searchImages: 'Search for images',
+		enterTextToSearch: 'Enter text to search for images',
+		search: 'Search',
+		emptyFolderAdded: 'No files were added from empty folder',
+		folderAlreadyAdded: 'The folder "%{folder}" was already added',
+		folderAdded: {
+			0: 'Added %{smart_count} file from %{folder}',
+			1: 'Added %{smart_count} files from %{folder}',
+		},
+	},
+};
+```
+
+#### `store`
+
+The store that is used to keep track of internal state (`Object`, default:
+[`DefaultStore`](/docs/guides/custom-stores)).
+
+This option can be used to plug Uppy state into an external state management
+library, such as [Redux](/docs/guides/custom-stores).
+
+{/* TODO document store API */}
+
+#### `infoTimeout`
+
+How long an [Informer](/docs/informer) notification will be visible (`number`,
+default: `5000`).
+
+### Methods
+
+#### `use(plugin, opts)`
+
+Add a plugin to Uppy, with an optional plugin options object.
+
+```js
+import Uppy from '@uppy/core';
+import DragDrop from '@uppy/drag-drop';
+
+const uppy = new Uppy();
+uppy.use(DragDrop, { target: 'body' });
+```
+
+#### `removePlugin(instance)`
+
+Uninstall and remove a plugin.
+
+#### `getPlugin(id)`
+
+Get a plugin by its `id` to access its methods.
+
+#### `getID()`
+
+Get the Uppy instance ID, see the [`id`](#id) option.
+
+#### `addFile(file)`
+
+Add a new file to Uppy’s internal state. `addFile` will return the generated id
+for the file that was added.
+
+`addFile` gives an error if the file cannot be added, either because
+`onBeforeFileAdded(file)` gave an error, or because `uppy.opts.restrictions`
+checks failed.
+
+```js
+uppy.addFile({
+	name: 'my-file.jpg', // file name
+	type: 'image/jpeg', // file type
+	data: blob, // file blob
+	meta: {
+		// optional, store the directory path of a file so Uppy can tell identical files in different directories apart.
+		relativePath: webkitFileSystemEntry.relativePath,
+	},
+	source: 'Local', // optional, determines the source of the file, for example, Instagram.
+	isRemote: false, // optional, set to true if actual file is not in the browser, but on some remote server, for example,
+	// when using companion in combination with Instagram.
+});
+```
+
+:::note
+
+If you try to add a file that already exists, `addFile` will throw an error.
+Unless that duplicate file was dropped with a folder — duplicate files from
+different folders are allowed, when selected with that folder. This is because
+we add `file.meta.relativePath` to the `file.id`.
+
+:::
+
+:::info
+
+Checkout [working with Uppy files](#working-with-uppy-files).
+
+:::
+
+:::info
+
+If `uppy.opts.autoProceed === true`, Uppy will begin uploading automatically
+when files are added.
+
+:::
+
+:::info
+
+Sometimes you might need to add a remote file to Uppy. This can be achieved by
+[fetching the file, then creating a Blob object, or using the Url plugin with Companion](https://github.com/transloadit/uppy/issues/1006#issuecomment-413495493).
+
+:::
+
+:::info
+
+Sometimes you might need to mark some files as “already uploaded”, so that the
+user sees them, but they won’t actually be uploaded by Uppy. This can be
+achieved by
+[looping through files and setting `uploadComplete: true, uploadStarted: true` on them](https://github.com/transloadit/uppy/issues/1112#issuecomment-432339569)
+
+:::
+
+#### `removeFile(fileID)`
+
+Remove a file from Uppy. Removing a file that is already being uploaded cancels
+that upload.
+
+```js
+uppy.removeFile('uppyteamkongjpg1501851828779');
+```
+
+#### `getFile(fileID)`
+
+Get a specific [Uppy file](#working-with-uppy-files) by its ID.
+
+```js
+const file = uppy.getFile('uppyteamkongjpg1501851828779');
+```
+
+#### `getFiles()`
+
+Get an array of all added [Uppy files](#working-with-uppy-files).
+
+```js
+const files = uppy.getFiles();
+```
+
+#### `upload()`
+
+Start uploading added files.
+
+Returns a Promise `result` that resolves with an object containing two arrays of
+uploaded files:
+
+- `result.successful` - Files that were uploaded successfully.
+- `result.failed` - Files that did not upload successfully. These files will
+  have a `.error` property describing what went wrong.
+
+```js
+uppy.upload().then((result) => {
+	console.info('Successful uploads:', result.successful);
+
+	if (result.failed.length > 0) {
+		console.error('Errors:');
+		result.failed.forEach((file) => {
+			console.error(file.error);
+		});
+	}
+});
+```
+
+#### `pauseResume(fileID)`
+
+Toggle pause/resume on an upload. Will only work if resumable upload plugin,
+such as [Tus](/docs/tus/), is used.
+
+#### `pauseAll()`
+
+Pause all uploads. Will only work if a resumable upload plugin, such as
+[Tus](/docs/tus/), is used.
+
+#### `resumeAll()`
+
+Resume all uploads. Will only work if resumable upload plugin, such as
+[Tus](/docs/tus/), is used.
+
+#### `retryUpload(fileID)`
+
+Retry an upload (after an error, for example).
+
+#### `retryAll()`
+
+Retry all uploads (after an error, for example).
+
+#### `cancelAll({ reason: 'user' })`
+
+| Argument | Type     | Description                                                                                                                                                                                                                                                                                                 |
+| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `reason` | `string` | The reason for canceling. Plugins can use this to provide different cleanup behavior (Transloadit plugin cancels an Assembly if user clicked on the “cancel” button). Possible values are: `user` (default) - The user has pressed “cancel”; `unmount` - The Uppy instance has been closed programmatically |
+
+Cancel all uploads, reset progress and remove all files.
+
+#### `setState(patch)`
+
+Update Uppy’s internal state. Usually, this method is called internally, but in
+some cases it might be useful to alter something directly, especially when
+implementing your own plugins.
+
+Uppy’s default state on initialization:
+
+```js
+const state = {
+	plugins: {},
+	files: {},
+	currentUploads: {},
+	capabilities: {
+		resumableUploads: false,
+	},
+	totalProgress: 0,
+	meta: { ...this.opts.meta },
+	info: {
+		isHidden: true,
+		type: 'info',
+		message: '',
+	},
+};
+```
+
+Updating state:
+
+```js
+uppy.setState({ smth: true });
+```
+
+:::note
+
+State in Uppy is considered to be immutable. When updating values, make sure not
+mutate them, but instead create copies. See
+[Redux docs](http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html)
+for more info on this.
+
+:::
+
+#### `getState()`
+
+Returns the current state from the [Store](#store).
+
+#### `setFileState(fileID, state)`
+
+Update the state for a single file. This is mostly useful for plugins that may
+want to store data on [Uppy files](#working-with-uppy-files), or need to pass
+file-specific configurations to other plugins that support it.
+
+`fileID` is the string file ID. `state` is an object that will be merged into
+the file’s state object.
+
+#### `setMeta(data)`
+
+Alters global `meta` object in state, the one that can be set in Uppy options
+and gets merged with all newly added files. Calling `setMeta` will also merge
+newly added meta data with files that had been selected before.
+
+```js
+uppy.setMeta({ resize: 1500, token: 'ab5kjfg' });
+```
+
+#### `setFileMeta(fileID, data)`
+
+Update metadata for a specific file.
+
+```js
+uppy.setFileMeta('myfileID', { resize: 1500 });
+```
+
+#### `setOptions(opts)`
+
+Change the options Uppy initialized with.
+
+```js
+const uppy = new Uppy();
+
+uppy.setOptions({
+	restrictions: { maxNumberOfFiles: 3 },
+	autoProceed: true,
+});
+
+uppy.setOptions({
+	locale: {
+		strings: {
+			cancel: 'Отмена',
+		},
+	},
+});
+```
+
+You can also change options for plugin:
+
+```js
+// Change width of the Dashboard drag-and-drop aread on the fly
+uppy.getPlugin('Dashboard').setOptions({
+	width: 300,
+});
+```
+
+#### `close({ reason: 'user' })`
+
+| Argument | Type     | Description                                                                                                                                                                                                                                                                                                 |
+| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `reason` | `string` | The reason for canceling. Plugins can use this to provide different cleanup behavior (Transloadit plugin cancels an Assembly if user clicked on the “cancel” button). Possible values are: `user` (default) - The user has pressed “cancel”; `unmount` - The Uppy instance has been closed programmatically |
+
+Uninstall all plugins and close down this Uppy instance. Also runs
+`uppy.cancelAll()` before uninstalling.
+
+#### `logout()`
+
+Calls `provider.logout()` on each remote provider plugin (Google Drive,
+Instagram, etc). Useful, for example, after your users log out of their account
+in your app — this will clean things up with Uppy cloud providers as well, for
+extra security.
+
+#### `log(message, type)`
+
+| Argument  | Type      | Description                 |
+| --------- | --------- | --------------------------- |
+| `message` | `string`  | message to log              |
+| `type`    | `string?` | `debug`, `warn`, or `error` |
+
+See [`logger`](#logger) docs for details.
+
+```js
+uppy.log('[Dashboard] adding files...');
+```
+
+#### `info(message, type, duration)`
+
+Sets a message in state, with optional details, that can be shown by
+notification UI plugins. It’s using the [Informer](/docs/informer) plugin,
+included by default in Dashboard.
+
+| Argument   | Type               | Description                                                                       |
+| ---------- | ------------------ | --------------------------------------------------------------------------------- |
+| `message`  | `string`, `Object` | `'info message'` or `{ message: 'Oh no!', details: 'File couldn’t be uploaded' }` |
+| `type`     | `string?`          | `'info'`, `'warning'`, `'success'` or `'error'`                                   |
+| `duration` | `number?`          | in milliseconds                                                                   |
+
+`info-visible` and `info-hidden` events are emitted when this info message
+should be visible or hidden.
+
+```js
+this.info('Oh my, something good happened!', 'success', 3000);
+```
+
+```js
+this.info(
+	{
+		message: 'Oh no, something bad happened!',
+		details:
+			'File couldn’t be uploaded because there is no internet connection',
+	},
+	'error',
+	5000,
+);
+```
+
+#### `addPreProcessor(fn)`
+
+Add a preprocessing function. `fn` gets called with a list of file IDs before an
+upload starts. `fn` should return a Promise. Its resolution value is ignored.
+
+:::info
+
+To change file data and such, use Uppy state updates, for example using
+[`setFileState`](#setfilestatefileid-state).
+
+:::
+
+#### `addUploader(fn)`
+
+Add an uploader function. `fn` gets called with a list of file IDs when an
+upload should start. Uploader functions should do the actual uploading work,
+such as creating and sending an XMLHttpRequest or calling into some upload
+service SDK. `fn` should return a Promise that resolves once all files have been
+uploaded.
+
+:::tip
+
+You may choose to still resolve the Promise if some file uploads fail. This way,
+any postprocessing will still run on the files that were uploaded successfully,
+while uploads that failed will be retried when [`retryAll`](#retryall) is
+called.
+
+:::
+
+#### `addPostProcessor(fn)`
+
+Add a postprocessing function. `fn` is called with a list of file IDs when an
+upload has finished. `fn` should return a Promise that resolves when the
+processing work is complete. The value of the Promise is ignored.
+
+For example, you could wait for file encoding or CDN propagation to complete, or
+you could do an HTTP API call to create an album containing all images that were
+uploaded.
+
+#### `removePreProcessor/removeUploader/removePostProcessor(fn)`
+
+Remove a processor or uploader function that was added before. Normally, this
+should be done in the [`uninstall()`](#uninstall) method.
+
+#### `on('event', action)`
+
+Subscribe to an uppy-event. See below for the full list of events.
+
+#### `once('event', action)`
+
+Create an event listener that fires once. See below for the full list of events.
+
+#### `off('event', action)`
+
+Unsubscribe to an uppy-event. See below for the full list of events.
+
+### Events
+
+Uppy exposes events that you can subscribe to for side-effects.
+
+#### `file-added`
+
+Fired each time a file is added.
+
+**Parameters**
+
+- `file` - The [Uppy file](#working-with-uppy-files) that was added.
+
+```js
+uppy.on('file-added', (file) => {
+	console.log('Added file', file);
+});
+```
+
+#### `files-added`
+
+**Parameters**
+
+- `files` - Array of [Uppy files](#working-with-uppy-files) which were added at
+  once, in a batch.
+
+Fired each time when one or more files are added — one event, for all files
+
+#### `file-removed`
+
+Fired each time a file is removed.
+
+**Parameters**
+
+- `file` - The [Uppy file](#working-with-uppy-files) that was removed.
+- `reason` - A string explaining why the file was removed. See
+  [#2301](https://github.com/transloadit/uppy/issues/2301#issue-628931176) for
+  details. Current reasons are: `removed-by-user` and `cancel-all`.
+
+**Example**
+
+```js
+uppy.on('file-removed', (file, reason) => {
+	console.log('Removed file', file);
+});
+```
+
+```js
+uppy.on('file-removed', (file, reason) => {
+	removeFileFromUploadingCounterUI(file);
+
+	if (reason === 'removed-by-user') {
+		sendDeleteRequestForFile(file);
+	}
+});
+```
+
+#### `upload`
+
+Fired when the upload starts.
+
+```js
+uppy.on('upload', (data) => {
+	// data object consists of `id` with upload ID and `fileIDs` array
+	// with file IDs in current upload
+	// data: { id, fileIDs }
+	console.log(`Starting upload ${id} for files ${fileIDs}`);
+});
+```
+
+#### `preprocess-progress`
+
+Progress of the pre-processors.
+
+**Parameters**
+
+`progress` is an object with properties:
+
+- `mode` - Either `'determinate'` or `'indeterminate'`.
+- `message` - A message to show to the user. Something like
+  `'Preparing upload...'`, but be more specific if possible.
+
+When `mode` is `'determinate'`, also add the `value` property:
+
+- `value` - A progress value between 0 and 1.
+
+#### `progress`
+
+Fired each time the total upload progress is updated:
+
+**Parameters**
+
+- `progress` - An integer (0-100) representing the total upload progress.
+
+**Example**
+
+```js
+uppy.on('progress', (progress) => {
+	// progress: integer (total progress percentage)
+	console.log(progress);
+});
+```
+
+#### `upload-progress`
+
+Fired each time an individual file upload progress is available:
+
+**Parameters**
+
+- `file` - The [Uppy file](#working-with-uppy-files) that has progressed.
+- `progress` - The same object as in `file.progress`.
+
+**Example**
+
+```js
+uppy.on('upload-progress', (file, progress) => {
+	// file: { id, name, type, ... }
+	// progress: { uploader, bytesUploaded, bytesTotal }
+	console.log(file.id, progress.bytesUploaded, progress.bytesTotal);
+});
+```
+
+#### `postprocess-progress`
+
+Progress of the post-processors.
+
+**Parameters**
+
+`progress` is an object with properties:
+
+- `mode` - Either `'determinate'` or `'indeterminate'`.
+- `message` - A message to show to the user. Something like
+  `'Preparing upload...'`, but be more specific if possible.
+
+When `mode` is `'determinate'`, also add the `value` property:
+
+- `value` - A progress value between 0 and 1.
+
+#### `upload-success`
+
+Fired each time a single upload is completed.
+
+**Parameters**
+
+- `file` - The [Uppy file](#working-with-uppy-files) that was uploaded.
+- `response` - An object with response data from the remote endpoint. The actual
+  contents depend on the upload plugin that is used.
+
+For `@uppy/xhr-upload`, the shape is:
+
+```json
+{
+	"status": 200, // HTTP status code (0, 200, 300)
+	"body": "…", // response body
+	"uploadURL": "…" // the file url, if it was returned
+}
+```
+
+**Example**
+
+```js
+uppy.on('upload-success', (file, response) => {
+	console.log(file.name, response.uploadURL);
+	const img = new Image();
+	img.width = 300;
+	img.alt = file.id;
+	img.src = response.uploadURL;
+	document.body.appendChild(img);
+});
+```
+
+#### `complete`
+
+Fired when all uploads are complete.
+
+The `result` parameter is an object with arrays of `successful` and `failed`
+files, as in [`uppy.upload()`](#upload)’s return value.
+
+```js
+uppy.on('complete', (result) => {
+	console.log('successful files:', result.successful);
+	console.log('failed files:', result.failed);
+});
+```
+
+#### `error`
+
+Fired when Uppy fails to upload/encode the entire upload.
+
+**Parameters**
+
+- `error` - The error object.
+
+**Example**
+
+```js
+uppy.on('error', (error) => {
+	console.error(error.stack);
+});
+```
+
+#### `upload-error`
+
+Fired each time a single upload failed.
+
+**Parameters**
+
+- `file` - The [Uppy file](#working-with-uppy-files) which didn’t upload.
+- `error` - The error object.
+- `response` - an optional parameter with response data from the upload
+  endpoint.
+
+It may be undefined or contain different data depending on the upload plugin in
+use.
+
+For `@uppy/xhr-upload`, the shape is:
+
+```json
+{
+	"status": 200, // HTTP status code (0, 200, 300)
+	"body": "…" // response body
+}
+```
+
+**Example**
+
+```js
+uppy.on('upload-error', (file, error, response) => {
+	console.log('error with file:', file.id);
+	console.log('error message:', error);
+});
+```
+
+If the error is related to network conditions — endpoint unreachable due to
+firewall or ISP blockage, for instance — the error will have
+`error.isNetworkError` property set to `true`. Here’s how you can check for
+network errors:
+
+```js
+uppy.on('upload-error', (file, error, response) => {
+	if (error.isNetworkError) {
+		// Let your users know that file upload could have failed
+		// due to firewall or ISP issues
+		alertUserAboutPossibleFirewallOrISPIssues(error);
+	}
+});
+```
+
+#### `upload-retry`
+
+Fired when an upload has been retried (after an error, for example).
+
+:::note
+
+This event is not triggered when the user retries all uploads, it will trigger
+the `retry-all` event instead.
+
+:::
+
+**Parameters**
+
+- `fileID` - ID of the file that is being retried.
+
+**Example**
+
+```js
+uppy.on('upload-retry', (fileID) => {
+	console.log('upload retried:', fileID);
+});
+```
+
+#### `upload-stalled`
+
+Fired when an upload has not received any progress in some time (in
+`@uppy/xhr-upload`, the delay is defined by the `timeout` option). Use this
+event to display a message on the UI to tell the user they might want to retry
+the upload.
+
+```js
+uppy.on('upload-stalled', (error, files) => {
+	console.log('upload seems stalled', error, files);
+	const noLongerStalledEventHandler = (file) => {
+		if (files.includes(file)) {
+			console.log('upload is no longer stalled');
+			uppy.off('upload-progress', noLongerStalledEventHandler);
+		}
+	};
+	uppy.on('upload-progress', noLongerStalledEventHandler);
+});
+```
+
+#### `retry-all`
+
+Fired when all failed uploads are retried
+
+**Parameters**
+
+- `fileIDs` - Arrays of IDs of the files being retried.
+
+**Example**
+
+```js
+uppy.on('retry-all', (fileIDs) => {
+	console.log('upload retried:', fileIDs);
+});
+```
+
+#### `info-visible`
+
+Fired when “info” message should be visible in the UI. By default, `Informer`
+plugin is displaying these messages (enabled by default in `Dashboard` plugin).
+You can use this event to show messages in your custom UI:
+
+```js
+uppy.on('info-visible', () => {
+	const { info } = uppy.getState();
+	// info: {
+	//  isHidden: false,
+	//  type: 'error',
+	//  message: 'Failed to upload',
+	//  details: 'Error description'
+	// }
+	console.log(`${info.message} ${info.details}`);
+});
+```
+
+#### `info-hidden`
+
+Fired when “info” message should be hidden in the UI. See
+[`info-visible`](#info-visible).
+
+#### `cancel-all`
+
+| Argument | Type     | Description                         |
+| -------- | -------- | ----------------------------------- |
+| `reason` | `string` | See [uppy.cancelAll](####cancelAll) |
+
+Fired when `cancelAll()` is called, all uploads are canceled, files removed and
+progress is reset.
+
+#### `restriction-failed`
+
+Fired when a file violates certain restrictions when added. This event is
+providing another choice for those who want to customize the behavior of file
+upload restrictions.
+
+```js
+uppy.on('restriction-failed', (file, error) => {
+	// do some customized logic like showing system notice to users
+});
+```
+
+#### `reset-progress`
+
+Fired when `resetProgress()` is called, each file has its upload progress reset
+to zero.
+
+```js
+uppy.on('reset-progress', () => {
+	// progress was reset
+});
+```
+
+## `new BasePlugin(uppy, options?)`
+
+The initial building block for a plugin.
+
+`BasePlugin` does not contain DOM rendering so it can be used for plugins
+without an user interface.
+
+:::info
+
+See [`UIPlugin`][] for the extended version with Preact rendering for
+interfaces.
+
+:::
+
+:::info
+
+Checkout the [building plugins](/docs/guides/building-plugins) guide.
+
+:::
+
+:::note
+
+If you don’t use any UI plugins and want to make sure Preact isn’t bundled into
+your app, import `BasePlugin` like this:
+`import BasePlugin from '@uppy/core/lib/BasePlugin`.
+
+:::
+
+### Options
+
+The options passed to `BasePlugin` are all you options you wish to support in
+your plugin.
+
+You should pass the options to `super` in your plugin class:
+
+```js
+class MyPlugin extends BasePlugin {
+	constructor(uppy, opts) {
+		super(uppy, opts);
+	}
+}
+```
+
+### Methods
+
+#### `setOptions(options)`
+
+Options passed during initialization can also be altered dynamically with
+`setOptions`.
+
+#### `getPluginState()`
+
+Retrieves the plugin state from the `Uppy` class. Uppy keeps a `plugins` object
+in state in which each key is the plugin’s `id`, and the value its state.
+
+#### `setPluginState()`
+
+Set the plugin state in the `Uppy` class. Uppy keeps a `plugins` object in state
+in which each key is the plugin’s `id`, and the value its state.
+
+#### `install()`
+
+The `install` method is ran once, when the plugin is added to Uppy with
+`.use()`. Use this to initialize the plugin.
+
+For example, if you are creating a pre-processor (such as
+[@uppy/compressor](/docs/compressor)) you must add it:
+
+```js
+install () {
+  this.uppy.addPreProcessor(this.prepareUpload)
+}
+```
+
+Another common thing to do when creating a
+[UI plugin](#new-uipluginuppy-options) is to [`mount`](#mounttarget) it to the
+DOM:
+
+```js
+install () {
+  const { target } = this.opts
+  if (target) {
+    this.mount(target, this)
+  }
+}
+```
+
+#### `uninstall()`
+
+The `uninstall` method is ran once, when the plugin is removed from Uppy. This
+happens when `.close()` is called or when the plugin is destroyed in a framework
+integration.
+
+Use this to clean things up.
+
+For instance when creating a pre-processor, uploader, or post-processor to
+remove it:
+
+```js
+uninstall () {
+  this.uppy.removePreProcessor(this.prepareUpload)
+}
+```
+
+When creating a [UI plugin](#new-uipluginuppy-options) you should
+[`unmount`](#unmount) it from the DOM:
+
+```js
+uninstall () {
+  this.unmount()
+}
+```
+
+#### `i18nInit`
+
+Call `this.i18nInit()` once in the constructor of your plugin class to
+initialize [internationalisation](/docs/locales).
+
+#### `addTarget`
+
+You can use this method to make your plugin a `target` for other plugins. This
+is what `@uppy/dashboard` uses to add other plugins to its UI.
+
+#### `update`
+
+Called on each state update. You will rarely need to use this, unless if you
+want to build a UI plugin using something other than Preact.
+
+#### `afterUpdate`
+
+Called after every state update with a debounce, after everything has mounted.
+
+## `new UIPlugin(uppy, options?)`
+
+`UIPlugin` extends [`BasePlugin`][] to add rendering with
+[Preact](https://preactjs.com/). Use this when you want to create an user
+interface or an addition to one, such as [Dashboard][].
+
+:::info
+
+See [`BasePlugin`][] for the initial building block for all plugins.
+
+:::
+
+:::info
+
+Checkout the [building plugins](/docs/guides/building-plugins) guide.
+
+:::
+
+### Options
+
+The options passed to `UIPlugin` are all you options you wish to support in your
+plugin.
+
+You should pass the options to `super` in your plugin class:
+
+```js
+class MyPlugin extends UIPlugin {
+	constructor(uppy, opts) {
+		super(uppy, opts);
+	}
+}
+```
+
+In turn these are also passed to the underlying `BasePlugin`.
+
+### Methods
+
+All the methods from [`BasePlugin`][] are also inherited into `UIPlugin`.
+
+#### `mount(target)`
+
+Mount this plugin to the `target` element. `target` can be a CSS query selector,
+a DOM element, or another Plugin. If `target` is a Plugin, the source (current)
+plugin will register with the target plugin, and the latter can decide how and
+where to render the source plugin.
+
+#### `onMount()`
+
+Called after Preact has rendered the components of the plugin.
+
+#### `unmount`
+
+Removing the plugin from the DOM. You generally don’t need to override it but
+you should call it from [`uninstall`](#uninstall).
+
+The default is:
+
+```js
+unmount () {
+  if (this.isTargetDOMEl) {
+    this.el?.remove()
+  }
+  this.onUnmount()
+}
+```
+
+#### `onUnmount()`
+
+Called after the elements have been removed from the DOM. Can be used to do some
+clean up or other side-effects.
+
+#### `render()`
+
+Render the UI of the plugin. Uppy uses [Preact](https://preactjs.com) as its
+view engine, so `render()` should return a Preact element. `render` is
+automatically called by Uppy on each state change.
+
+#### `update(state)`
+
+Called on each state update. You will rarely need to use this, unless if you
+want to build a UI plugin using something other than Preact.
+
+## `debugLogger()`
+
+Logger with extra debug and warning logs for during development.
+
+```js
+import { Uppy, debugLogger } from '@uppy/core';
+
+new Uppy({ logger: debugLogger });
+```
+
+:::info
+
+You can also enable this logger by setting [`debug`](#debug) to `true`.
+
+:::
+
+The default value of [`logger`](#logger) is `justErrorsLogger`, which looks like
+this:
+
+```js
+// Swallow all logs, except errors.
+// default if logger is not set or debug: false
+const justErrorsLogger = {
+	debug: () => {},
+	warn: () => {},
+	error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+};
+```
+
+`debugLogger` sends extra debugging and warning logs which could be helpful
+during development:
+
+```js
+// Print logs to console with namespace + timestamp,
+// set by logger: Uppy.debugLogger or debug: true
+const debugLogger = {
+	debug: (...args) => console.debug(`[Uppy] [${getTimeStamp()}]`, ...args),
+	warn: (...args) => console.warn(`[Uppy] [${getTimeStamp()}]`, ...args),
+	error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+};
+```
+
+## Frequently asked questions
+
+### How do I allow duplicate files?
+
+You can allow all files, even duplicate files, with
+[`onBeforeFileAdded`](#onbeforefileadded). This will override the file if it has
+not been uploaded. If you upload a duplicate file again it depends on your
+upload plugin and backend how it is handled.
+
+```js
+const uppy = new Uppy({
+  // ...
+  onBeforeFileAdded: () => true,
+```
+
+[dashboard]: /docs/dashboard
+[`baseplugin`]: #new-basepluginuppy-options
+[`uiplugin`]: #new-uipluginuppy-options

+ 4 - 0
docs/user-interfaces/_category_.json

@@ -0,0 +1,4 @@
+{
+	"label": "User interfaces",
+	"position": 5
+}

+ 753 - 0
docs/user-interfaces/dashboard.mdx

@@ -0,0 +1,753 @@
+---
+sidebar_position: 1
+slug: /dashboard
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Dashboard
+
+The all you need Dashboard — powerful, responsive, and pluggable. Kickstart your
+uploading experience and gradually add more functionality. Add files from
+[remote sources](/docs/companion), [edit images](/docs/image-editor),
+[generate thumbnails](/docs/thumbnail-generator), and more.
+
+Checkout [integrations](#integrations) for the full list of plugins you can
+integrate.
+
+:::tip
+
+[Try out the live example with all plugins](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+
+:::
+
+## When should I use this?
+
+There could be many reasons why you may want to use the Dashboard, but some
+could be:
+
+- when you need a battle tested plug-and-play uploading UI to save time.
+- when your users need to add files from [remote sources](/docs/companion), such
+  [Google Drive](/docs/google-drive), [Dropbox](/docs/dropbox), and others.
+- when you need to collect [meta data](#metafields) from your users per file.
+- when your users want to take a picture with their [webcam](/docs/webcam) or
+  [capture their screen](/docs/screen-capture).
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/core @uppy/dashboard
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/core @uppy/dashboard
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dashboard } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dashboard, { target: '#uppy', inline: true })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+new Uppy().use(Dashboard, { inline: true, target: '#uppy-dashboard' });
+```
+
+:::note
+
+The `@uppy/dashboard` plugin includes CSS for the Dashboard itself, and the
+various plugins used by the Dashboard, such as
+([`@uppy/status-bar`](/docs/status-bar) and [`@uppy/informer`](/docs/informer)).
+If you also use the `@uppy/status-bar` or `@uppy/informer` plugin directly, you
+should not include their CSS files, but instead only use the one from the
+`@uppy/dashboard` plugin.
+
+:::
+
+:::note
+
+Styles for Provider plugins, like Google Drive and Instagram, are also bundled
+with Dashboard styles. Styles for other plugins, such as `@uppy/url` and
+`@uppy/webcam`, are not included. If you are using those, please see their docs
+and make sure to include styles for them as well.
+
+:::
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'Dashboard'`).
+
+Plugins that are added by the Dashboard get unique IDs based on this ID, like
+`'Dashboard:StatusBar'` and `'Dashboard:Informer'`.
+
+#### `target`
+
+Where to render the Dashboard (`string` or `Element`, default: `'body'`).
+
+You can pass an element, class, or id as a string. Dashboard is rendered into
+`body`, because it’s hidden by default and only opened as a modal when `trigger`
+is clicked.
+
+#### `inline`
+
+Render the Dashboard as a modal or inline (`boolean`, default: `false`).
+
+When `false`, Dashboard is opened by clicking on [`trigger`](#trigger). If
+`inline: true`, Dashboard will be rendered into [`target`](#target) and fit
+right in.
+
+#### `trigger`
+
+A CSS selector for a button that will trigger opening the Dashboard modal
+(`string`, default: `null`).
+
+Several buttons or links can be used, as long as they are selected using the
+same selector (`.select-file-button`, for example).
+
+#### `width`
+
+Width of the Dashboard in pixels (`number`, default: `750`). Used when
+`inline: true`.
+
+#### `height`
+
+Height of the Dashboard in pixels (`number`, default: `550`). Used when
+`inline: true`.
+
+#### `waitForThumbnailsBeforeUpload`
+
+Whether to wait for all thumbnails from `@uppy/thumbnail-generator` to be ready
+before starting the upload (`boolean`, default `false`).
+
+If set to `true`, Thumbnail Generator will envoke Uppy’s internal processing
+stage, displaying “Generating thumbnails...” message, and wait for
+`thumbnail:all-generated` event, before proceeding to the uploading stage.
+
+This is useful because Thumbnail Generator also adds EXIF data to images, and if
+we wait until it’s done processing, this data will be available on the server
+after the upload.
+
+#### `showLinkToFileUploadResult`
+
+Turn the file icon and thumbnail in the Dashboard into a link to the uploaded
+file (`boolean`, default: `false`).
+
+Please make sure to return the `url` key (or the one set via
+`responseUrlFieldName`) from your server.
+
+#### `showProgressDetails`
+
+Show or hide progress details in the status bar (`boolean`, default: `false`).
+
+By default, progress in Status Bar is shown as a percentage. If you would like
+to also display remaining upload size and time, set this to `true`.
+
+`showProgressDetails: false`: Uploading: 45%
+
+`showProgressDetails: true`: Uploading: 45%・43 MB of 101 MB・8s left
+
+#### `hideUploadButton`
+
+Show or hide the upload button (`boolean`, default: `false`).
+
+Use this if you are providing a custom upload button somewhere, and are using
+the `uppy.upload()` API.
+
+#### `hideRetryButton`
+
+Hide the retry button in the status bar and on each individual file (`boolean`,
+default: `false`).
+
+Use this if you are providing a custom retry button somewhere and if you are
+using the `uppy.retryAll()` or `uppy.retryUpload(fileID)` API.
+
+#### `hidePauseResumeButton`
+
+Hide the pause/resume button (for resumable uploads, via [tus](http://tus.io),
+for example) in the status bar and on each individual file (`boolean`, default:
+`false`).
+
+Use this if you are providing custom cancel or pause/resume buttons somewhere,
+and using the `uppy.pauseResume(fileID)` or `uppy.removeFile(fileID)` API.
+
+#### `hideCancelButton`
+
+Hide the cancel button in status bar and on each individual file (`boolean`,
+default: `false`).
+
+Use this if you are providing a custom retry button somewhere, and using the
+`uppy.cancelAll()` API.
+
+#### `hideProgressAfterFinish`
+
+Hide the status bar after the upload has finished (`boolean`, default: `false`).
+
+#### `doneButtonHandler`
+
+This option is passed to the status bar and will render a “Done” button in place
+of pause/resume/cancel buttons, once the upload/encoding is done. The behaviour
+of this “Done” button is defined by this handler function, for instance to close
+the file picker modals or clear the upload state.
+
+This is what the Dashboard sets by default:
+
+```js
+const doneButtonHandler = () => {
+	this.uppy.cancelAll();
+	this.requestCloseModal();
+};
+```
+
+Set to `null` to disable the “Done” button.
+
+#### `showSelectedFiles`
+
+Show the list of added files with a preview and file information (`boolean`,
+default: `true`).
+
+In case you are showing selected files in your own app’s UI and want the Uppy
+Dashboard to only be a picker, the list can be hidden with this option.
+
+See also [`disableStatusBar`](#disablestatusbar) option, which can hide the
+progress and upload button.
+
+#### `showRemoveButtonAfterComplete`
+
+Show the remove button on every file after a successful upload (`boolean`,
+default: `false`).
+
+Enabling this option only shows the remove `X` button in the Dashboard UI, but
+to actually send a request you should listen to
+[`file-removed`](https://uppy.io/docs/uppy/#file-removed) event and add your
+logic there.
+
+Example:
+
+```js
+uppy.on('file-removed', (file, reason) => {
+	if (reason === 'removed-by-user') {
+		sendDeleteRequestForFile(file);
+	}
+});
+```
+
+For an implementation example, please see
+[#2301](https://github.com/transloadit/uppy/issues/2301#issue-628931176).
+
+#### `singleFileFullScreen`
+
+When only one file is selected, its preview and meta information will be
+centered and enlarged (`boolean`, default: `true`).
+
+Often times Uppy used for photo / profile image uploads, or maybe a single
+document. Then it makes sense to occupy the whole space of the available
+Dashboard UI, giving the stage to this one file. This feature is automatically
+disabled when Dashboard is small in height, since there’s not enough room.
+
+#### `note`
+
+A string of text to be placed in the Dashboard UI (`string`, default: `null`).
+
+This could for instance be used to explain any [`restrictions`](#restrictions)
+that are put in place. For example:
+`'Images and video only, 2–3 files, up to 1 MB'`.
+
+#### `metaFields`
+
+Create text or custom input fields for the user to fill in (`Array<Object>` or
+`Function`, default: `null`).
+
+This will be shown when a user clicks the “edit” button on that file.
+
+:::note
+
+The meta data will only be set on a file object if it’s entered by the user. If
+the user doesn’t edit a file’s metadata, it will not have default values;
+instead everything will be `undefined`. If you want to set a certain meta field
+to each file regardless of user actions, set
+[`meta` in the Uppy constructor options](/docs/uppy/#meta).
+
+:::
+
+Each object can contain:
+
+- `id`. The name of the meta field. This will also be used in CSS/HTML as part
+  of the `id` attribute, so it’s better to
+  [avoid using characters like periods, semicolons, etc](https://stackoverflow.com/a/79022).
+- `name`. The label shown in the interface.
+- `placeholder`. The text shown when no value is set in the field. (Not needed
+  when a custom render function is provided)
+- `render: ({value, onChange, required, form}, h) => void` (optional). A
+  function for rendering a custom form element.
+  - `value` is the current value of the meta field
+  - `onChange: (newVal) => void` is a function saving the new value and `h` is
+    the `createElement` function from
+    [Preact](https://preactjs.com/guide/v10/api-reference#h--createelement).
+  - `required` is a boolean that’s true if the field `id` is in the
+    `restrictedMetaFields` restriction
+  - `form` is the `id` of the associated `<form>` element.
+  - `h` can be useful when using Uppy from plain JavaScript, where you cannot
+    write JSX.
+
+<details>
+<summary>Example: meta fields configured as an `Array`</summary>
+
+```js
+uppy.use(Dashboard, {
+	trigger: '#pick-files',
+	metaFields: [
+		{ id: 'name', name: 'Name', placeholder: 'file name' },
+		{ id: 'license', name: 'License', placeholder: 'specify license' },
+		{
+			id: 'caption',
+			name: 'Caption',
+			placeholder: 'describe what the image is about',
+		},
+		{
+			id: 'public',
+			name: 'Public',
+			render({ value, onChange, required, form }, h) {
+				return h('input', {
+					type: 'checkbox',
+					required,
+					form,
+					onChange: (ev) => onChange(ev.target.checked ? 'on' : ''),
+					defaultChecked: value === 'on',
+				});
+			},
+		},
+	],
+});
+```
+
+</details>
+
+<details>
+<summary>Example: dynamic meta fields based on file type with a `Function`</summary>
+
+```js
+uppy.use(Dashboard, {
+	trigger: '#pick-files',
+	metaFields: (file) => {
+		const fields = [{ id: 'name', name: 'File name' }];
+		if (file.type.startsWith('image/')) {
+			fields.push({ id: 'location', name: 'Photo Location' });
+			fields.push({ id: 'alt', name: 'Alt text' });
+			fields.push({
+				id: 'public',
+				name: 'Public',
+				render: ({ value, onChange, required, form }, h) => {
+					return h('input', {
+						type: 'checkbox',
+						onChange: (ev) => onChange(ev.target.checked ? 'on' : ''),
+						defaultChecked: value === 'on',
+						required,
+						form,
+					});
+				},
+			});
+		}
+		return fields;
+	},
+});
+```
+
+</details>
+
+#### `closeModalOnClickOutside`
+
+Set to true to automatically close the modal when the user clicks outside of it
+(`boolean`, default: `false`).
+
+#### `closeAfterFinish`
+
+Set to true to automatically close the modal when all current uploads are
+complete (`boolean`, default: `false`).
+
+With this option, the modal is only automatically closed when uploads are
+complete _and successful_. If some uploads failed, the modal stays open so the
+user can retry failed uploads or cancel the current batch and upload an entirely
+different set of files instead.
+
+:::info
+
+You can use this together with the
+[`allowMultipleUploads: false`](/docs/uppy/#allowmultipleuploads) option in Uppy
+Core to create a smooth experience when uploading a single (batch of) file(s).
+
+This is recommended. With several upload batches, the auto-closing behavior can
+be quite confusing for users.
+
+:::
+
+#### `disablePageScrollWhenModalOpen`
+
+Disable page scroll when the modal is open (`boolean`, default: `true`).
+
+Page scrolling is disabled by default when the Dashboard modal is open, so when
+you scroll a list of files in Uppy, the website in the background stays still.
+Set to false to override this behaviour and leave page scrolling intact.
+
+#### `animateOpenClose`
+
+Add animations when the modal dialog is opened or closed, for a more satisfying
+user experience (`boolean`, default: `true`).
+
+#### `fileManagerSelectionType`
+
+Configure the type of selections allowed when browsing your file system via the
+file manager selection window (`string`, default: `'files'`).
+
+May be either `'files'`, `'folders'`, or `'both'`. Selecting entire folders for
+upload may not be supported on all
+[browsers](https://caniuse.com/#feat=input-file-directory).
+
+#### `proudlyDisplayPoweredByUppy`
+
+Show the Uppy logo with a link (`boolean`, default: `true`).
+
+Uppy is provided to the world for free by the team behind
+[Transloadit](https://transloadit.com). In return, we ask that you consider
+keeping a tiny Uppy logo at the bottom of the Dashboard, so that more people can
+discover and use Uppy.
+
+#### `disableStatusBar`
+
+Disable the status bar completely (`boolean`, default: `false`).
+
+Dashboard ships with the `StatusBar` plugin that shows upload progress and
+pause/resume/cancel buttons. If you want, you can disable the StatusBar to
+provide your own custom solution.
+
+#### `disableInformer`
+
+Disable informer (shows notifications in the form of toasts) completely
+(`boolean`, default: `false`).
+
+Dashboard ships with the `Informer` plugin that notifies when the browser is
+offline, or when it’s time to say cheese if `Webcam` is taking a picture. If you
+want, you can disable the Informer and/or provide your own custom solution.
+
+#### `disableThumbnailGenerator`
+
+Disable the thumbnail generator completely (`boolean`, default: `false`).
+
+Dashboard ships with the `ThumbnailGenerator` plugin that adds small resized
+image thumbnails to images, for preview purposes only. If you want, you can
+disable the `ThumbnailGenerator` and/or provide your own custom solution.
+
+#### `locale`
+
+```js
+module.exports = {
+	strings: {
+		// When `inline: false`, used as the screen reader label for the button that closes the modal.
+		closeModal: 'Close Modal',
+		// Used as the screen reader label for the plus (+) button that shows the “Add more files” screen
+		addMoreFiles: 'Add more files',
+		addingMoreFiles: 'Adding more files',
+		// Used as the header for import panels, e.g., “Import from Google Drive”.
+		importFrom: 'Import from %{name}',
+		// When `inline: false`, used as the screen reader label for the dashboard modal.
+		dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
+		// When `inline: true`, used as the screen reader label for the dashboard area.
+		dashboardTitle: 'Uppy Dashboard',
+		// Shown in the Informer when a link to a file was copied to the clipboard.
+		copyLinkToClipboardSuccess: 'Link copied to clipboard.',
+		// Used when a link cannot be copied automatically — the user has to select the text from the
+		// input element below this string.
+		copyLinkToClipboardFallback: 'Copy the URL below',
+		// Used as the hover title and screen reader label for buttons that copy a file link.
+		copyLink: 'Copy link',
+		back: 'Back',
+		// Used as the screen reader label for buttons that remove a file.
+		removeFile: 'Remove file',
+		// Used as the screen reader label for buttons that open the metadata editor panel for a file.
+		editFile: 'Edit file',
+		// Shown in the panel header for the metadata editor. Rendered as “Editing image.png”.
+		editing: 'Editing %{file}',
+		// Used as the screen reader label for the button that saves metadata edits and returns to the
+		// file list view.
+		finishEditingFile: 'Finish editing file',
+		saveChanges: 'Save changes',
+		// Used as the label for the tab button that opens the system file selection dialog.
+		myDevice: 'My Device',
+		dropHint: 'Drop your files here',
+		// Used as the hover text and screen reader label for file progress indicators when
+		// they have been fully uploaded.
+		uploadComplete: 'Upload complete',
+		uploadPaused: 'Upload paused',
+		// Used as the hover text and screen reader label for the buttons to resume paused uploads.
+		resumeUpload: 'Resume upload',
+		// Used as the hover text and screen reader label for the buttons to pause uploads.
+		pauseUpload: 'Pause upload',
+		// Used as the hover text and screen reader label for the buttons to retry failed uploads.
+		retryUpload: 'Retry upload',
+		// Used as the hover text and screen reader label for the buttons to cancel uploads.
+		cancelUpload: 'Cancel upload',
+		// Used in a title, how many files are currently selected
+		xFilesSelected: {
+			0: '%{smart_count} file selected',
+			1: '%{smart_count} files selected',
+		},
+		uploadingXFiles: {
+			0: 'Uploading %{smart_count} file',
+			1: 'Uploading %{smart_count} files',
+		},
+		processingXFiles: {
+			0: 'Processing %{smart_count} file',
+			1: 'Processing %{smart_count} files',
+		},
+		// The "powered by Uppy" link at the bottom of the Dashboard.
+		poweredBy: 'Powered by %{uppy}',
+		addMore: 'Add more',
+		editFileWithFilename: 'Edit file %{file}',
+		save: 'Save',
+		cancel: 'Cancel',
+		dropPasteFiles: 'Drop files here or %{browseFiles}',
+		dropPasteFolders: 'Drop files here or %{browseFolders}',
+		dropPasteBoth: 'Drop files here, %{browseFiles} or %{browseFolders}',
+		dropPasteImportFiles: 'Drop files here, %{browseFiles} or import from:',
+		dropPasteImportFolders: 'Drop files here, %{browseFolders} or import from:',
+		dropPasteImportBoth:
+			'Drop files here, %{browseFiles}, %{browseFolders} or import from:',
+		importFiles: 'Import files from:',
+		browseFiles: 'browse files',
+		browseFolders: 'browse folders',
+		recoveredXFiles: {
+			0: 'We could not fully recover 1 file. Please re-select it and resume the upload.',
+			1: 'We could not fully recover %{smart_count} files. Please re-select them and resume the upload.',
+		},
+		recoveredAllFiles: 'We restored all files. You can now resume the upload.',
+		sessionRestored: 'Session restored',
+		reSelect: 'Re-select',
+		missingRequiredMetaFields: {
+			0: 'Missing required meta field: %{fields}.',
+			1: 'Missing required meta fields: %{fields}.',
+		},
+	},
+};
+```
+
+#### `theme`
+
+Light or dark theme for the Dashboard (`string`, default: `'light'`).
+
+Uppy Dashboard supports “Dark Mode”. You can try it live on
+[the Dashboard example page](https://uppy.io/examples/).
+
+It supports the following values:
+
+- `light` — the default
+- `dark`
+- `auto` — will respect the user’s system settings and switch automatically
+
+#### `autoOpen`
+
+Automatically open file editor for the file user just dropped/selected.  
+If one file is added, editor opens for that file; if 10 files are added, editor
+opens only for the first file.
+
+This option supports the following values:
+
+- `null` - the default
+- `"metaEditor"` - open the meta fields editor if
+  [meta fields](/docs/dashboard/#metafields) are enabled.
+- `"imageEditor"` - open [`@uppy/image-editor`](/docs/image-editor) if the
+  plugin is enabled.
+
+#### `disabled`
+
+Enabling this option makes the Dashboard grayed-out and non-interactive
+(`boolean`, default: `false`).
+
+Users won’t be able to click on buttons or drop files. Useful when you need to
+conditionally enable/disable file uploading or manipulation, based on a
+condition in your app. Can be set on init or via API:
+
+```js
+const dashboard = uppy.getPlugin('Dashboard');
+dashboard.setOptions({ disabled: true });
+
+userNameInput.addEventListener('change', () => {
+	dashboard.setOptions({ disabled: false });
+});
+```
+
+#### `disableLocalFiles`
+
+Disable local files (`boolean`, default: `false`).
+
+Enabling this option will disable drag & drop, hide the “browse” and “My Device”
+button, allowing only uploads from plugins, such as Webcam, Screen Capture,
+Google Drive, Instagram.
+
+#### `onDragOver(event)`
+
+Callback for the [`ondragover`][ondragover] event handler.
+
+#### `onDrop(event)`
+
+Callback for the [`ondrop`][ondrop] event handler.
+
+#### `onDragLeave(event)`
+
+Callback for the [`ondragleave`][ondragleave] event handler.
+
+### Methods
+
+:::info
+
+Dashboard also has the methods described in
+[`UIPlugin`](/docs/uppy#new-uipluginuppy-options) and
+[`BasePlugin`](/docs/uppy#new-basepluginuppy-options).
+
+:::
+
+#### `openModal()`
+
+Shows the Dashboard modal. Use it like this:
+
+`uppy.getPlugin('Dashboard').openModal()`
+
+#### `closeModal()`
+
+Hides the Dashboard modal. Use it like this:
+
+`uppy.getPlugin('Dashboard').closeModal()`
+
+#### `isModalOpen()`
+
+Returns `true` if the Dashboard modal is open, `false` otherwise.
+
+```js
+const dashboard = uppy.getPlugin('Dashboard');
+if (dashboard.isModalOpen()) {
+	dashboard.closeModal();
+}
+```
+
+### Events
+
+:::info
+
+You can use [`on`](/docs/uppy#onevent-action) and
+[`once`](/docs/uppy#onceevent-action) to listen to these events.
+
+:::
+
+#### `dashboard:modal-open`
+
+Fired when the Dashboard modal is open.
+
+```js
+uppy.on('dashboard:modal-open', () => {
+	console.log('Modal is open');
+});
+```
+
+#### `dashboard:modal-closed`
+
+Fired when the Dashboard modal is closed.
+
+#### `dashboard:file-edit-start`
+
+**Parameters:**
+
+- `file` — The [File Object](https://uppy.io/docs/uppy/#File-Objects)
+  representing the file that was opened for editing.
+
+Fired when the user clicks “edit” icon next to a file in the Dashboard. The
+FileCard panel is then open with file metadata available for editing.
+
+#### `dashboard:file-edit-complete`
+
+**Parameters:**
+
+- `file` — The [File Object](https://uppy.io/docs/uppy/#File-Objects)
+  representing the file that was edited.
+
+Fired when the user finished editing the file metadata.
+
+## Integrations
+
+These are the plugins specifically made for the Dashboard. This is not a list of
+all Uppy plugins.
+
+### Sources
+
+- [`@uppy/audio`](/docs/audio) — record audio.
+- [`@uppy/box`](/docs/box) — import files from
+  [Box](https://www.box.com/en-nl/home).
+- [`@uppy/dropbox`](/docs/dropbox) — import from [Dropbox](https://dropbox.com).
+- [`@uppy/facebook`](/docs/facebook) — import from
+  [Facebook](https://facebook.com).
+- [`@uppy/google-drive`](/docs/google-drive) — import from
+  [Google Drive](https://drive.google.com).
+- [`@uppy/instagram`](/docs/instagram) — import from
+  [Instagram](https://instagram.com).
+- [`@uppy/onedrive`](/docs/onedrive) — import from
+  [OneDrive](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage).
+- [`@uppy/screen-capture`](/docs/screen-capture) — Record your screen, including
+  (optionally) your microphone.
+- [`@uppy/unsplash`](/docs/unsplash) — import files from
+  [Unsplash](https://unsplash.com/)
+- [`@uppy/url`](/docs/url) — import files from any URL.
+- [`@uppy/webcam`](/docs/webcam) — Record or make a picture with your webcam.
+- [`@uppy/zoom`](/docs/zoom) — import files from [Zoom](https://zoom.us).
+
+### UI
+
+- [`@uppy/image-editor`](/docs/image-editor) — allows users to crop, rotate,
+  zoom and flip images that are added to Uppy.
+- [`@uppy/informer`](/docs/informer) — show notifications.
+- [`@uppy/status-bar`](/docs/status-bar) — advanced upload progress status bar.
+- [`@uppy/thumbnail-generator`](/docs/thumbnail-generator) — generate preview
+  thumbnails for images to be uploaded.
+
+### Frameworks
+
+- [`@uppy/angular`](/docs/angular) — Dashboard component for
+  [Angular](https://angular.io/).
+- [`@uppy/react`](/docs/react) — Dashboard component for
+  [React](https://reactjs.org/).
+- [`@uppy/svelte`](/docs/svelte) — Dashboard component for
+  [Svelte](https://svelte.dev/).
+- [`@uppy/vue`](/docs/vue) — Dashboard component for [Vue](https://vuejs.org/).
+
+[ondragover]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragover
+[ondragleave]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragleave
+[ondrop]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondrop

+ 149 - 0
docs/user-interfaces/drag-drop.mdx

@@ -0,0 +1,149 @@
+---
+sidebar_position: 2
+slug: /drag-drop
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Drag & Drop
+
+The `@uppy/drag-drop` plugin renders a drag and drop area for file selection.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js).
+
+:::
+
+## When should I use this?
+
+It can be useful when you only want the local device as a file source, don’t
+need file previews and a UI for metadata editing, or the
+[Dashboard](/docs/dashboard/) is too much. But it can be too minimal too. By
+default it doesn’t show that a file has been added nor is there a progress bar.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/core @uppy/drag-drop
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/core @uppy/drag-drop
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, DragDrop } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(DragDrop, { target: '#uppy' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js showLineNumbers
+import Uppy from '@uppy/core';
+import DragDrop from '@uppy/drag-drop';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/drag-drop/dist/style.min.css';
+
+new Uppy().use(DragDrop, { target: '#drag-drop' });
+```
+
+:::info
+
+Certain [restrictions](/docs/uppy#restrictions) set in Uppy’s options, namely
+`maxNumberOfFiles` and `allowedFileTypes`, affect the system file picker dialog.
+If `maxNumberOfFiles: 1`, users will only be able to select one file, and
+`allowedFileTypes: ['video/*', '.gif']` means only videos or gifs (files with
+`.gif` extension) will be selectable.
+
+:::
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, Default: `'DragDrop'`).
+
+Use this if you need to add several DragDrop instances.
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string` or `Element`, default: `null`).
+
+#### `width`
+
+Drag and drop area width (`string`, default: `'100%'`).
+
+Set in inline CSS, so feel free to use percentage, pixels or other values that
+you like.
+
+#### `height`
+
+Drag and drop area height (`string`, default: `'100%'`).
+
+Set in inline CSS, so feel free to use percentage, pixels or other values that
+you like.
+
+#### `note`
+
+Optionally, specify a string of text that explains something about the upload
+for the user (`string`, default: `null`).
+
+This is a place to explain any `restrictions` that are put in place. For
+example: `'Images and video only, 2–3 files, up to 1 MB'`.
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		// Text to show on the droppable area.
+		// `%{browse}` is replaced with a link that opens the system file selection dialog.
+		dropHereOr: 'Drop here or %{browse}',
+		// Used as the label for the link that opens the system file selection dialog.
+		browse: 'browse',
+	},
+};
+```
+
+#### `onDragOver(event)`
+
+Callback for the [`ondragover`][ondragover] event handler.
+
+#### `onDragLeave(event)`
+
+Callback for the [`ondragleave`][ondragleave] event handler.
+
+#### `onDrop(event)`
+
+Callback for the [`ondrop`][ondrop] event handler.
+
+[ondragover]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragover
+[ondragleave]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragleave
+[ondrop]:
+	https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondrop

+ 5 - 0
docs/user-interfaces/elements/_category_.json

@@ -0,0 +1,5 @@
+{
+	"label": "Elements",
+	"position": 3,
+	"collapsed": false
+}

+ 113 - 0
docs/user-interfaces/elements/drop-target.mdx

@@ -0,0 +1,113 @@
+---
+sidebar_position: 2
+slug: /drop-target
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Drop target
+
+The `@uppy/drop-target` plugin lets your users drag-and-drop files on any
+element on the page, for example the whole page, `document.body`.
+
+Can be used together with Uppy Dashboard or Drag & Drop plugins, or your custom
+solution targeting any DOM element.
+
+:::tip
+
+[Try out the live example](/examples) or take it for a spin in
+[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js).
+
+:::
+
+## When should I use this?
+
+When you want to allow users to drag and drop files in your own UI, rather than
+in the [`Dashboard`](/docs/dashboard) UI, or catch dropped files from anywhere
+on the page.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/drop-target
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/drop-target
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { DropTarget } from "{{UPPY_JS_URL}}"
+        const DropTarget = new Uppy().use(DropTarget)
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+This module has one default export: the `DropTarget` plugin class.
+
+```js {8-10} showLineNumbers
+import Uppy from '@uppy/core';
+import DropTarget from '@uppy/drop-target';
+
+import '@uppy/core/dist/style.css';
+import '@uppy/drop-target/dist/style.css';
+
+const uppy = new Uppy();
+uppy.use(DropTarget, {
+	target: document.body,
+});
+```
+
+## API
+
+### Options
+
+#### `onDragLeave`
+
+Event listener for the [`dragleave` event][].
+
+```js {3-5} showLineNumbers
+uppy.use(DropTarget, {
+	target: document.body,
+	onDragLeave: (event) => {
+		event.stopPropagation();
+	},
+});
+```
+
+#### `onDragOver`
+
+Event listener for the [`dragover` event][].
+
+#### `onDrop`
+
+Event listener for the [`drop` event][].
+
+#### `target`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into
+(`string`, `Element`, `Function`, or `UIPlugin`, default: `null`).
+
+[`dragover` event]:
+	https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragover_event
+[`dragleave` event]:
+	https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragleave_event
+[`drop` event]:
+	https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event

+ 179 - 0
docs/user-interfaces/elements/image-editor.mdx

@@ -0,0 +1,179 @@
+---
+sidebar_position: 1
+slug: /image-editor
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Image editor
+
+Image editor. Designed to be used with the Dashboard UI.
+
+<div style={{ maxWidth: 500 }}>
+
+![Screenshot of the Image Editor plugin UI in Dashboard](https://user-images.githubusercontent.com/1199054/87208710-654db400-c307-11ea-9471-6e3c6582d2a5.png)
+
+</div>
+
+## When should I use this?
+
+When you want to allow users to crop, rotate, zoom and flip images that are
+added to Uppy.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/core @uppy/dashboard @uppy/image-editor
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/core @uppy/dashboard @uppy/image-editor
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Dashboard, ImageEditor } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Dashboard, { target: '#uppy', inline: true })
+        uppy.use(ImageEditor, { target: Uppy.Dashboard })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js {3,7,11} showLineNumbers
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import ImageEditor from '@uppy/image-editor';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+import '@uppy/image-editor/dist/style.min.css';
+
+new Uppy()
+	.use(Dashboard, { inline: true, target: '#dashboard' })
+	.use(ImageEditor, { target: Dashboard });
+```
+
+## API
+
+### Options
+
+:::info
+
+If you automatically want to open the image editor when an image is added, see
+the [`autoOpen`](/docs/dashboard#autoopen) Dashboard option.
+
+:::
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'ImageEditor'`).
+
+#### `quality`
+
+Quality Of the resulting blob that will be saved in Uppy after editing/cropping
+(`number`, default: `0.8`).
+
+#### `cropperOptions`
+
+Image Editor is using the excellent
+[Cropper.js](https://fengyuanchen.github.io/cropperjs/). `cropperOptions` will
+be directly passed to `Cropper` and thus can expect the same values as
+documented in their
+[README](https://github.com/fengyuanchen/cropperjs/blob/HEAD/README.md#options),
+with the addition of `croppedCanvasOptions`, which will be passed to
+[`getCroppedCanvas`](https://github.com/fengyuanchen/cropperjs/blob/HEAD/README.md#getcroppedcanvasoptions).
+
+#### `actions`
+
+Show action buttons (`Object` or `boolean`).
+
+If you you’d like to hide all actions, pass `false` to it. By default all the
+actions are visible. Or enable/disable them individually:
+
+```js
+{
+  revert: true,
+  rotate: true,
+  granularRotate: true,
+  flip: true,
+  zoomIn: true,
+  zoomOut: true,
+  cropSquare: true,
+  cropWidescreen: true,
+  cropWidescreenVertical: true,
+}
+```
+
+#### `locale: {}`
+
+```js
+export default {
+	strings: {
+		revert: 'Revert',
+		rotate: 'Rotate',
+		zoomIn: 'Zoom in',
+		zoomOut: 'Zoom out',
+		flipHorizontal: 'Flip horizontal',
+		aspectRatioSquare: 'Crop square',
+		aspectRatioLandscape: 'Crop landscape (16:9)',
+		aspectRatioPortrait: 'Crop portrait (9:16)',
+	},
+};
+```
+
+### Events
+
+:::info
+
+You can use [`on`](/docs/uppy#onevent-action) and
+[`once`](/docs/uppy#onceevent-action) to listen to these events.
+
+:::
+
+#### `file-editor:start`
+
+Emitted when `selectFile(file)` is called.
+
+```js
+uppy.on('file-editor:start', (file) => {
+	console.log(file);
+});
+```
+
+#### `file-editor:complete`
+
+Emitted after `save(blob)` is called.
+
+```js
+uppy.on('file-editor:complete', (updatedFile) => {
+	console.log(updatedFile);
+});
+```
+
+#### `file-editor:cancel`
+
+Emitted when `uninstall` is called or when the current image editing changes are
+discarded.
+
+```js
+uppy.on('file-editor:cancel', (file) => {
+	console.log(file);
+});
+```

+ 94 - 0
docs/user-interfaces/elements/informer.mdx

@@ -0,0 +1,94 @@
+---
+sidebar_position: 3
+slug: /informer
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Informer
+
+The `@uppy/informer` plugin is a pop-up bar for showing notifications for the
+[Dashboard](/docs/dashboard). When plugins have some exciting news (or errors)
+to share, they can with Informer
+
+## When should I use it?
+
+When you use the [Dashboard](/docs/dashboard) it’s already included by default.
+This plugin is published separately but made specifically for the Dashboard. You
+can technically use it without it, but it’s not officially supported.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/informer
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/informer
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, Informer } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(Informer, { target: '#informer' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js
+import Uppy from '@uppy/core';
+import Informer from '@uppy/informer';
+
+// The `@uppy/informer` plugin includes some basic styles.
+// You can also choose not to use it and provide your own styles instead.
+import '@uppy/core/dist/style.min.css';
+import '@uppy/informer/dist/style.min.css';
+
+new Uppy().use(Informer, { target: '#informer' });
+```
+
+Informer gets its data from `uppy.state.info`, which is updated by various
+plugins via [`uppy.info`](/docs/uppy#infomessage-type-duration) method.
+
+In the [compressor](/docs/compressor) plugin we use it like this for instance:
+
+```js
+const size = prettierBytes(totalCompressedSize);
+this.uppy.info(this.i18n('compressedX', { size }), 'info');
+```
+
+When calling `uppy.info`, [Uppy](/docs/uppy) emits
+[`info-visible`](/docs/uppy#info-visible) and will emit
+[`info-hidden`](/docs/uppy#info-hidden) after the timeout.
+
+## API
+
+### Options
+
+### `id`
+
+A unique identifier for this plugin (`string`, default: `'Informer'`).
+
+Use this if you need several `Informer` instances.
+
+### `target`
+
+DOM element, CSS selector, or plugin to mount the Informer into (`string` or
+`Element`, default: `null`).

+ 91 - 0
docs/user-interfaces/elements/progress-bar.mdx

@@ -0,0 +1,91 @@
+---
+sidebar_position: 5
+slug: /progress-bar
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Progress bar
+
+`@uppy/progress-bar` is a minimalist plugin that shows the current upload
+progress in a thin bar element, like the ones used by YouTube and GitHub when
+navigating between pages.
+
+## When should I use it?
+
+When you need a minimalistec progress indicator and you’re
+[building your own UI](/docs/guides/building-your-own-ui-with-uppy). For most
+cases, [Dashboard](/docs/dashboard) or [Drag & Drop](/docs/drag-drop) (with
+[Status Bar](/docs/status-bar)) is more likely what you need.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/progress-bar
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/progress-bar
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, ProgressBar } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(ProgressBar, { target: '#progress-bar' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js
+import Uppy from '@uppy/core';
+import ProgressBar from '@uppy/progress-bar';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/progress-bar/dist/style.min.css';
+
+new Uppy().use(ProgressBar, { target: '#progress-bar' });
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this Progress Bar (`string`, default: `'ProgressBar'`).
+
+Use this if you need to add several ProgressBar instances.
+
+#### `target`
+
+DOM element, CSS selector, or plugin to mount the Progress Bar into (`Element`,
+`string`, default: `null`).
+
+#### `fixed`
+
+Show the progress bar at the top of the page with `position: fixed` (`boolean`,
+default: `false`).
+
+When set to false, show the progress bar inline wherever it’s mounted.
+
+#### `hideAfterFinish`
+
+Hides the progress bar after the upload has finished (`boolean`, default:
+`true`).

+ 201 - 0
docs/user-interfaces/elements/status-bar.mdx

@@ -0,0 +1,201 @@
+---
+sidebar_position: 4
+slug: /status-bar
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Status bar
+
+The `@uppy/status-bar` plugin shows upload progress and speed, estimated time,
+pre- and post-processing information, and allows users to control
+(pause/resume/cancel) the upload.
+
+:::tip
+
+Try it out together with [`@uppy/drag-drop`](/docs/drag-drop) in
+[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js)
+
+:::
+
+## When should I use it?
+
+When you use the [Dashboard](/docs/dashboard) it’s already included by default.
+This plugin is published separately but made specifically for the Dashboard. You
+can still use it without it but it may need some (CSS) tweaking for your use
+case.
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/status-bar
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/status-bar
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, StatusBar } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(StatusBar, { target: '#status-bar' })
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+```js showLineNumbers
+import Uppy from '@uppy/core';
+import StatusBar from '@uppy/status-bar';
+
+import '@uppy/core/dist/style.min.css';
+import '@uppy/status-bar/dist/style.min.css';
+
+new Uppy().use(StatusBar, { target: '#status-bar' });
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this Status Bar (`string`, default: `'StatusBar'`).
+
+Use this if you need to add several StatusBar instances.
+
+#### `target`
+
+DOM element, CSS selector, or plugin to mount the Status Bar into (`Element`,
+`string`, `UIPlugin`, default: `'body'`).
+
+#### `hideAfterFinish`
+
+Hide the Status Bar after the upload is complete (`boolean`, default: `true`).
+
+#### `showProgressDetails`
+
+Display remaining upload size and estimated time (`boolean`, default: `false`)
+
+:::note
+
+`false`: Uploading: 45%
+
+`true`: Uploading: 45%・43 MB of 101 MB・8s left
+
+:::
+
+#### `hideUploadButton`
+
+Hide the upload button (`boolean`, default: `false`).
+
+Use this if you are providing a custom upload button somewhere, and using the
+`uppy.upload()` API.
+
+#### `hideRetryButton`
+
+Hide the retry button (`boolean`, default: `false`).
+
+Use this if you are providing a custom retry button somewhere, and using the
+`uppy.retryAll()` or `uppy.retryUpload(fileID)` API.
+
+#### `hidePauseResumeButton`
+
+Hide pause/resume buttons (for resumable uploads, via [tus](http://tus.io), for
+example) (`boolean`, default: `false`).
+
+Use this if you are providing custom cancel or pause/resume buttons somewhere,
+and using the `uppy.pauseResume(fileID)` or `uppy.removeFile(fileID)` API.
+
+#### `hideCancelButton`
+
+Hide the cancel button (`boolean`, default: `false`).
+
+Use this if you are providing a custom retry button somewhere, and using the
+`uppy.cancelAll()` API.
+
+#### `doneButtonHandler`
+
+Status Bar will render a “Done” button in place of pause/resume/cancel buttons,
+once the upload/encoding is done (`Function`, default: `null`).
+
+The behaviour of this “Done” button is defined by the handler function — can be
+used to close file picker modals or clear the upload state. This is what the
+Dashboard plugin, which uses Status Bar internally, sets:
+
+```js
+const doneButtonHandler = () => {
+	this.uppy.reset();
+	this.requestCloseModal();
+};
+```
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		// Shown in the status bar while files are being uploaded.
+		uploading: 'Uploading',
+		// Shown in the status bar once all files have been uploaded.
+		complete: 'Complete',
+		// Shown in the status bar if an upload failed.
+		uploadFailed: 'Upload failed',
+		// Shown in the status bar while the upload is paused.
+		paused: 'Paused',
+		// Used as the label for the button that retries an upload.
+		retry: 'Retry',
+		// Used as the label for the button that cancels an upload.
+		cancel: 'Cancel',
+		// Used as the label for the button that pauses an upload.
+		pause: 'Pause',
+		// Used as the label for the button that resumes an upload.
+		resume: 'Resume',
+		// Used as the label for the button that resets the upload state after an upload
+		done: 'Done',
+		// When `showProgressDetails` is set, shows the number of files that have been fully uploaded so far.
+		filesUploadedOfTotal: {
+			0: '%{complete} of %{smart_count} file uploaded',
+			1: '%{complete} of %{smart_count} files uploaded',
+		},
+		// When `showProgressDetails` is set, shows the amount of bytes that have been uploaded so far.
+		dataUploadedOfTotal: '%{complete} of %{total}',
+		// When `showProgressDetails` is set, shows an estimation of how long the upload will take to complete.
+		xTimeLeft: '%{time} left',
+		// Used as the label for the button that starts an upload.
+		uploadXFiles: {
+			0: 'Upload %{smart_count} file',
+			1: 'Upload %{smart_count} files',
+		},
+		// Used as the label for the button that starts an upload, if another upload has been started in the past
+		// and new files were added later.
+		uploadXNewFiles: {
+			0: 'Upload +%{smart_count} file',
+			1: 'Upload +%{smart_count} files',
+		},
+		upload: 'Upload',
+		retryUpload: 'Retry upload',
+		xMoreFilesAdded: {
+			0: '%{smart_count} more file added',
+			1: '%{smart_count} more files added',
+		},
+		showErrorDetails: 'Show error details',
+	},
+};
+```

+ 168 - 0
docs/user-interfaces/elements/thumbnail-generator.mdx

@@ -0,0 +1,168 @@
+---
+sidebar_position: 2
+slug: /thumbnail-generator
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import UppyCdnExample from '/src/components/UppyCdnExample';
+
+# Thumbnail generator
+
+`@uppy/thumbnail-generator` generates proportional thumbnails (file previews)
+for images that are added to Uppy.
+
+## When should I use this?
+
+This plugin is included by default with the [Dashboard](/docs/dashboard) plugin,
+and can also be useful to display image previews in a custom UI.
+
+:::note
+
+Thumbnails are only generated for _local_ files. Remote files through
+[Companion](/docs/companion) generally won’t have thumbnails because they are
+downloaded on the server. Some providers, such as Google Drive, have baked in
+thumbnails into their API responses.
+
+:::
+
+## Install
+
+<Tabs>
+  <TabItem value="npm" label="NPM" default>
+
+```shell
+npm install @uppy/thumbnail-generator
+```
+
+  </TabItem>
+
+  <TabItem value="yarn" label="Yarn">
+
+```shell
+yarn add @uppy/thumbnail-generator
+```
+
+  </TabItem>
+
+  <TabItem value="cdn" label="CDN">
+    <UppyCdnExample>
+      {`
+        import { Uppy, ThumbnailGenerator } from "{{UPPY_JS_URL}}"
+        const uppy = new Uppy()
+        uppy.use(ThumbnailGenerator)
+      `}
+    </UppyCdnExample>
+  </TabItem>
+</Tabs>
+
+## Use
+
+If you use the [Dashboard](/docs/dashboard) then it’s already installed. If you
+want to use it programmatically for your own UI:
+
+```js showLineNumbers
+import Uppy from '@uppy/core';
+import ThumbnailGenerator from '@uppy/thumbnail-generator';
+
+const uppy = new Uppy();
+
+uppy.use(ThumbnailGenerator);
+uppy.on('thumbnail:generated', (file, preview) => doSomething(file, preview));
+```
+
+## API
+
+### Options
+
+#### `id`
+
+A unique identifier for this plugin (`string`, default: `'ThumbnailGenerator'`).
+
+#### `locale`
+
+```js
+export default {
+	strings: {
+		generatingThumbnails: 'Generating thumbnails...',
+	},
+};
+```
+
+#### `thumbnailWidth`
+
+Width of the resulting thumbnail (`number`, default: `200`).
+
+Thumbnails are always proportional and not cropped. If width is provided, height
+is calculated automatically to match ratio. If both width and height are given,
+only width is taken into account.
+
+#### `thumbnailHeight`
+
+Height of the resulting thumbnail (`number`, default: `200`).
+
+Thumbnails are always proportional and not cropped. If height is provided, width
+is calculated automatically to match ratio. If both width and height are given,
+only width is taken into account.
+
+:::note
+
+Produce a 300px height thumbnail with calculated width to match ratio:
+
+```js
+uppy.use(ThumbnailGenerator, { thumbnailHeight: 300 });
+```
+
+Produce a 300px width thumbnail with calculated height to match ratio (and
+ignore the given height):
+
+```js
+uppy.use(ThumbnailGenerator, { thumbnailWidth: 300, thumbnailHeight: 300 });
+```
+
+See issue [#979](https://github.com/transloadit/uppy/issues/979) and
+[#1096](https://github.com/transloadit/uppy/pull/1096) for details on this
+feature.
+
+:::
+
+#### `thumbnailType`
+
+MIME type of the resulting thumbnail (`string`, default: `'image/jpeg'`).
+
+This is useful if you want to support transparency in your thumbnails by
+switching to `image/png`.
+
+#### `waitForThumbnailsBeforeUpload`
+
+Whether to wait for all thumbnails to be ready before starting the upload
+(`boolean`, default: `false`).
+
+If set to `true`, Thumbnail Generator will invoke Uppy’s internal processing
+stage and wait for `thumbnail:all-generated` event, before proceeding to the
+uploading stage. This is useful because Thumbnail Generator also adds EXIF data
+to images, and if we wait until it’s done processing, this data will be
+available on the server after the upload.
+
+### Events
+
+:::info
+
+You can use [`on`](/docs/uppy#onevent-action) and
+[`once`](/docs/uppy#onceevent-action) to listen to these events.
+
+:::
+
+#### `thumbnail:generated`
+
+Emitted with `file` and `preview` local url as arguments:
+
+```js
+uppy.on('thumbnail:generated', (file, preview) => {
+	const img = document.createElement('img');
+	img.src = preview;
+	img.width = 100;
+	document.body.appendChild(img);
+});
+```

+ 18 - 8
examples/custom-provider/client/MyCustomProvider.jsx

@@ -1,25 +1,35 @@
 /** @jsx h */
 
 import { UIPlugin } from '@uppy/core'
-import { Provider } from '@uppy/companion-client'
+import { Provider, getAllowedHosts, tokenStorage } from '@uppy/companion-client'
 import { ProviderViews } from '@uppy/provider-views'
 import { h } from 'preact'
 
 const defaultOptions = {}
 
 export default class MyCustomProvider extends UIPlugin {
-  constructor (uppy, opts) {
+  constructor(uppy, opts) {
     super(uppy, opts)
     this.type = 'acquirer'
     this.id = this.opts.id || 'MyCustomProvider'
-    Provider.initPlugin(this, opts)
+    this.type = 'acquirer'
+    this.storage = this.opts.storage || tokenStorage
+    this.files = []
 
     this.icon = () => (
       <svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
-        <path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z" fill="#000000" fillRule="nonzero" />
+        <path
+          d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z"
+          fill="#000000"
+          fillRule="nonzero"
+        />
       </svg>
     )
 
+    this.opts.companionAllowedHosts = getAllowedHosts(
+      this.opts.companionAllowedHosts,
+      this.opts.companionUrl,
+    )
     this.provider = new Provider(uppy, {
       companionUrl: this.opts.companionUrl,
       companionHeaders: this.opts.companionHeaders,
@@ -44,7 +54,7 @@ export default class MyCustomProvider extends UIPlugin {
     this.files = []
   }
 
-  install () {
+  install() {
     this.view = new ProviderViews(this, {
       provider: this.provider,
     })
@@ -55,16 +65,16 @@ export default class MyCustomProvider extends UIPlugin {
     }
   }
 
-  uninstall () {
+  uninstall() {
     this.view.tearDown()
     this.unmount()
   }
 
-  onFirstRender () {
+  onFirstRender() {
     return this.view.getFolder()
   }
 
-  render (state) {
+  render(state) {
     return this.view.render(state)
   }
 }

+ 7 - 0
packages/@uppy/dashboard/CHANGELOG.md

@@ -12,6 +12,13 @@ Included in: Uppy v4.0.0-beta.1
 - @uppy/dashboard: refactor to TypeScript (Antoine du Hamel / #4984)
 - @uppy/dashboard: refactor to stable lifecycle method (Antoine du Hamel / #4999)
 
+## 3.8.1
+
+Released: 2024-04-16
+Included in: Uppy v3.24.3
+
+- @uppy/dashboard: add missing `x-zip-compress` archive type (Younes / #5081)
+
 ## 3.8.0
 
 Released: 2024-03-27

+ 1 - 0
packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx

@@ -193,6 +193,7 @@ export default function getIconByMime(fileType: $TSFixMe): $TSFixMe {
   const archiveTypes = [
     'zip',
     'x-7z-compressed',
+    'x-zip-compressed',
     'x-rar-compressed',
     'x-tar',
     'x-gzip',

+ 7 - 0
packages/@uppy/utils/CHANGELOG.md

@@ -11,6 +11,13 @@ Included in: Uppy v4.0.0-beta.1
 - @uppy/utils: fix `AbortablePromise` type (Antoine du Hamel / #4988)
 - @uppy/utils: migrate RateLimitedQueue to TS (Merlijn Vos / #4981)
 
+## 5.8.0
+
+Released: 2024-04-16
+Included in: Uppy v3.24.3
+
+- @uppy/utils: add fetcher (Merlijn Vos / #5073)
+
 ## 5.7.5
 
 Released: 2024-03-27

+ 145 - 0
packages/@uppy/utils/src/fetcher.ts

@@ -0,0 +1,145 @@
+import NetworkError from './NetworkError.ts'
+import ProgressTimeout from './ProgressTimeout.ts'
+
+const noop = (): void => {}
+
+export type FetcherOptions = {
+  /** The HTTP method to use for the request. Default is 'GET'. */
+  method?: string
+
+  /** The request payload, if any. Default is null. */
+  body?: Document | XMLHttpRequestBodyInit | null
+
+  /** Milliseconds between XMLHttpRequest upload progress events before the request is aborted. Default is 30000 ms. */
+  timeout?: number
+
+  /** Sets the withCredentials property of the XMLHttpRequest object. Default is false. */
+  withCredentials?: boolean
+
+  /** Sets the responseType property of the XMLHttpRequest object. Default is an empty string. */
+  responseType?: XMLHttpRequestResponseType
+
+  /** An object representing any headers to send with the request. */
+  headers?: Record<string, string>
+
+  /** The number of retry attempts to make if the request fails. Default is 3. */
+  retries?: number
+
+  /** Called before the request is made. */
+  onBeforeRequest?: (
+    xhr: XMLHttpRequest,
+    retryCount: number,
+  ) => void | Promise<void>
+
+  /** Function for tracking upload progress. */
+  onUploadProgress?: (event: ProgressEvent) => void
+
+  /** A function to determine whether to retry the request. */
+  shouldRetry?: (xhr: XMLHttpRequest) => boolean
+
+  /** Called after the response has succeeded or failed but before the promise is resolved. */
+  onAfterRequest?: (
+    xhr: XMLHttpRequest,
+    retryCount: number,
+  ) => void | Promise<void>
+
+  /** Called when no XMLHttpRequest upload progress events have been received for `timeout` ms. */
+  onTimeout?: () => void
+
+  /** Signal to abort the upload. */
+  signal?: AbortSignal
+}
+
+/**
+ * Fetches data from a specified URL using XMLHttpRequest, with optional retry functionality and progress tracking.
+ *
+ * @param url The URL to send the request to.
+ * @param options Optional settings for the fetch operation.
+ */
+export function fetcher(
+  url: string,
+  options: FetcherOptions = {},
+): Promise<XMLHttpRequest> {
+  const {
+    body = null,
+    headers = {},
+    method = 'GET',
+    onBeforeRequest = noop,
+    onUploadProgress = noop,
+    shouldRetry = () => true,
+    onAfterRequest = noop,
+    onTimeout = noop,
+    responseType,
+    retries = 3,
+    signal = null,
+    timeout = 30_000,
+    withCredentials = false,
+  } = options
+
+  // 300 ms, 600 ms, 1200 ms, 2400 ms, 4800 ms
+  const delay = (attempt: number): number => 0.3 * 2 ** (attempt - 1) * 1000
+  const timer = new ProgressTimeout(timeout, onTimeout)
+
+  function requestWithRetry(retryCount = 0): Promise<XMLHttpRequest> {
+    // eslint-disable-next-line no-async-promise-executor
+    return new Promise(async (resolve, reject) => {
+      const xhr = new XMLHttpRequest()
+
+      xhr.open(method, url, true)
+      xhr.withCredentials = withCredentials
+      if (responseType) {
+        xhr.responseType = responseType
+      }
+
+      signal?.addEventListener('abort', () => {
+        xhr.abort()
+        // Using DOMException for abort errors aligns with
+        // the convention established by the Fetch API.
+        reject(new DOMException('Aborted', 'AbortError'))
+      })
+
+      xhr.onload = async () => {
+        await onAfterRequest(xhr, retryCount)
+
+        if (xhr.status >= 200 && xhr.status < 300) {
+          timer.done()
+          resolve(xhr)
+        } else if (shouldRetry(xhr) && retryCount < retries) {
+          setTimeout(() => {
+            requestWithRetry(retryCount + 1).then(resolve, reject)
+          }, delay(retryCount))
+        } else {
+          timer.done()
+          reject(new NetworkError(xhr.statusText, xhr))
+        }
+      }
+
+      xhr.onerror = () => {
+        if (shouldRetry(xhr) && retryCount < retries) {
+          setTimeout(() => {
+            requestWithRetry(retryCount + 1).then(resolve, reject)
+          }, delay(retryCount))
+        } else {
+          timer.done()
+          reject(new NetworkError(xhr.statusText, xhr))
+        }
+      }
+
+      xhr.upload.onprogress = (event: ProgressEvent) => {
+        timer.progress()
+        onUploadProgress(event)
+      }
+
+      if (headers) {
+        Object.keys(headers).forEach((key) => {
+          xhr.setRequestHeader(key, headers[key])
+        })
+      }
+
+      await onBeforeRequest(xhr, retryCount)
+      xhr.send(body)
+    })
+  }
+
+  return requestWithRetry()
+}