浏览代码

Merge branch 'master' of github.com:transloadit/uppy

Ifedapo Olarewaju 6 年之前
父节点
当前提交
12e5939041
共有 75 个文件被更改,包括 1328 次插入942 次删除
  1. 25 8
      CHANGELOG.md
  2. 3 0
      packages/@uppy/aws-s3-multipart/src/index.js
  3. 8 52
      packages/@uppy/core/src/_common.scss
  4. 1 1
      packages/@uppy/core/src/_variables.scss
  5. 4 2
      packages/@uppy/dashboard/package.json
  6. 0 100
      packages/@uppy/dashboard/src/FileCard.js
  7. 0 60
      packages/@uppy/dashboard/src/FileList.js
  8. 0 78
      packages/@uppy/dashboard/src/Tabs.js
  9. 2 2
      packages/@uppy/dashboard/src/components/ActionBrowseTagline.js
  10. 101 0
      packages/@uppy/dashboard/src/components/AddFiles.js
  11. 21 0
      packages/@uppy/dashboard/src/components/AddFilesPanel.js
  12. 35 27
      packages/@uppy/dashboard/src/components/Dashboard.js
  13. 111 0
      packages/@uppy/dashboard/src/components/FileCard.js
  14. 31 31
      packages/@uppy/dashboard/src/components/FileItem.js
  15. 0 0
      packages/@uppy/dashboard/src/components/FileItemProgress.js
  16. 23 0
      packages/@uppy/dashboard/src/components/FileList.js
  17. 1 1
      packages/@uppy/dashboard/src/components/FilePreview.js
  18. 28 0
      packages/@uppy/dashboard/src/components/PanelContent.js
  19. 31 0
      packages/@uppy/dashboard/src/components/PanelTopBar.js
  20. 4 18
      packages/@uppy/dashboard/src/components/icons.js
  21. 47 9
      packages/@uppy/dashboard/src/index.js
  22. 274 207
      packages/@uppy/dashboard/src/style.scss
  23. 0 0
      packages/@uppy/dashboard/src/utils/copyToClipboard.js
  24. 0 0
      packages/@uppy/dashboard/src/utils/copyToClipboard.test.js
  25. 1 1
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.js
  26. 17 0
      packages/@uppy/dashboard/src/utils/ignoreEvent.js
  27. 0 0
      packages/@uppy/dashboard/src/utils/truncateString.js
  28. 0 0
      packages/@uppy/dashboard/src/utils/truncateString.test.js
  29. 1 1
      packages/@uppy/drag-drop/package.json
  30. 0 14
      packages/@uppy/dropbox/src/icons.js
  31. 3 4
      packages/@uppy/dropbox/src/index.js
  32. 11 5
      packages/@uppy/google-drive/src/index.js
  33. 6 7
      packages/@uppy/informer/src/index.js
  34. 36 21
      packages/@uppy/informer/src/style.scss
  35. 5 7
      packages/@uppy/instagram/src/index.js
  36. 4 8
      packages/@uppy/provider-views/src/AuthView.js
  37. 3 3
      packages/@uppy/provider-views/src/Breadcrumbs.js
  38. 8 6
      packages/@uppy/provider-views/src/Browser.js
  39. 26 2
      packages/@uppy/provider-views/src/Item.js
  40. 144 83
      packages/@uppy/provider-views/src/style.scss
  41. 4 5
      packages/@uppy/status-bar/src/StatusBar.js
  42. 77 39
      packages/@uppy/status-bar/src/style.scss
  43. 21 24
      packages/@uppy/url/src/index.js
  44. 3 2
      packages/@uppy/url/src/style.scss
  45. 1 1
      packages/@uppy/webcam/src/CameraIcon.js
  46. 2 3
      packages/@uppy/webcam/src/PermissionsScreen.js
  47. 7 3
      packages/@uppy/webcam/src/index.js
  48. 24 15
      packages/@uppy/webcam/src/style.scss
  49. 8 4
      website/src/docs/aws-s3-multipart.md
  50. 25 21
      website/src/docs/aws-s3.md
  51. 11 3
      website/src/docs/dashboard.md
  52. 1 1
      website/src/docs/dragdrop.md
  53. 11 3
      website/src/docs/dropbox.md
  54. 3 3
      website/src/docs/fileinput.md
  55. 2 2
      website/src/docs/form.md
  56. 6 4
      website/src/docs/golden-retriever.md
  57. 10 2
      website/src/docs/google-drive.md
  58. 3 3
      website/src/docs/informer.md
  59. 11 3
      website/src/docs/instagram.md
  60. 3 3
      website/src/docs/progressbar.md
  61. 6 4
      website/src/docs/providers.md
  62. 4 2
      website/src/docs/react-dashboard-modal.md
  63. 5 3
      website/src/docs/react-dashboard.md
  64. 2 0
      website/src/docs/react-dragdrop.md
  65. 2 0
      website/src/docs/react-progressbar.md
  66. 2 0
      website/src/docs/react-statusbar.md
  67. 4 2
      website/src/docs/react.md
  68. 4 4
      website/src/docs/redux.md
  69. 2 0
      website/src/docs/server.md
  70. 4 4
      website/src/docs/statusbar.md
  71. 19 15
      website/src/docs/transloadit.md
  72. 4 0
      website/src/docs/uppy.md
  73. 12 4
      website/src/docs/url.md
  74. 9 1
      website/src/docs/webcam.md
  75. 1 1
      website/src/docs/xhrupload.md

+ 25 - 8
CHANGELOG.md

@@ -66,11 +66,13 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] webcam: Stop recording when file size is exceeded, should be possible given how the MediaRecorder API works
 - [ ] dashboard: add option to disable uploading from local disk #657
 - [ ] dashboard: display data like image resolution on file cards #783
+- [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
 - [ ] server: pass metadata to S3 `getKey` option, see https://github.com/transloadit/uppy/issues/689
 - [ ] core: I think there is a use case for having a single-use mode or something for Uppy, where pressing "Upload" locks it down (no new files can be added) and once the upload is finished it's just done. especially with the Form plugin
 - [ ] dashboard: hiding pause/resume from the UI by default (with option) would be good too probably (we could auto pause and show a resume button when detecting a network change to a metered network using https://devdocs.io/dom/networkinformation/type)
 - [ ] test: Add a prepublish test that checks if `npm pack` is not massive
 - [ ] Add release documentation. eg: test on transloadit website, check examples on the uppy.io website
+- [ ] dashboard: add image cropping, study https://github.com/MattKetmo/darkroomjs/, https://github.com/fengyuanchen/cropperjs #151
 
 ## 1.0 Goals
 
@@ -87,7 +89,7 @@ What we need to do to release Uppy 1.0
 - [ ] QA: add one integration test that uses a Provider (investigate if possible with a dedicated Google Drive API key for uppy server, so _with_ oauth dance) (@ife)
 - [ ] QA: add one integration test that uses more exotic (tus) options such as `useFastRemoteRetry` (@arturi)
 - [ ] feature: preset for Transloadit that mimics jQuery SDK, check https://github.com/transloadit/jquery-sdk docs (@goto-bus-stop)
-- [ ] dashboard: implement Alex' redesign (@arturi)
+- [x] dashboard: implement Alex and Artur’s Dashboard redesign (@arturi)
 - [ ] feature: basic React Native support (@arturi owner+ios, @ife android)
 - [ ] refactoring: Make `uppy-server` module live in main Uppy repo in `./server` as a second stage todo (after Lerna is done and we're happy) (@ife)
 - [x] QA: add one integration test that uses a Webpack and React/Redux environment (e.g. via `create-react-app`) (@goto-bus-stop)
@@ -108,9 +110,7 @@ What we need to do to release Uppy 1.0
 - [x] uppy-server: security audit
 - [x] uppy-server: storing tokens in user’s browser only (d040281cc9a63060e2f2685c16de0091aee5c7b4)
 
-# next
-
-## 0.27.0
+## 0.28.0
 
 - [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
 - [ ] core: customizing metadata fields, boolean metadata; see #809, #454 and related (@arturi)
@@ -120,17 +120,34 @@ What we need to do to release Uppy 1.0
 - [ ] dragdrop: allow customizing arrow icon https://github.com/transloadit/uppy/pull/374#issuecomment-334116208 (@arturi)
 - [ ] dashboard: cancel button for transloadit assemblies (@arturi, @goto-bus-stop)
 - [ ] dashboard: optional alert `onbeforeunload` while upload is in progress, safeguarding from accidentaly navigating away from a page with an ongoing upload
-- [ ] dashboard: add image cropping, study https://github.com/MattKetmo/darkroomjs/, https://github.com/fengyuanchen/cropperjs #151
 - [ ] docs: quick start guide: https://community.transloadit.com/t/quick-start-guide-would-be-really-helpful/14605 (@arturi)
 - [ ] transloadit: add error reporting (@goto-bus-stop)
-- [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
 - [ ] core: use Browserslist config to share between PostCSS, Autoprefixer and Babel https://github.com/browserslist/browserslist, https://github.com/amilajack/eslint-plugin-compat (@arturi)
 - [ ] core: utilize https://github.com/jonathantneal/postcss-preset-env, maybe https://github.com/jonathantneal/postcss-normalize (@arturi)
-- [ ] core: default `autoProceed` to `false` (@arturi)
+
+# next
+
+## 0.27.0
+
+To Be Released: 2018-08-07.
+
+- [x] core: default `autoProceed` to `false` (#961 / @arturi)
+- [x] core: fix `setPluginState` (#968 / @goto-bus-stop)
 - [x] website: list bundle sizes for each package on stats page (#962 / @goto-bus-stop)
 - [x] s3: Abort all chunk requests when aborting the multipart upload (#967 / @pekala)
 - [x] s3: Catch and handle errors in prepareUploadPart (#966 / @pekala)
-- [x] Split integration tests and add one using create-react-app (#952)
+- [x] Split integration tests and add one using create-react-app (#952 / @goto-bus-stop)
+- [x] ⚠️ **breaking** dashboard: UI overhaul: AddFiles panel, significantly improved mobile styles,  (#942 / @arturi, @nqst)
+- [x] ⚠️ **breaking** dashboard: Introduce `.uppy-size--md` and `.uppy-size--lg` breakpoint classes; throttle the function that checks for width (#942 / @arturi)
+- [x] dashboard: downgrade `drag-drop` module to support folders again (#942 / @arturi)
+- [x] dashboard: fix animation — wait for closing animation to finish before opening modal (#942 / @arturi)
+- [x] webcam: add webcam permission screen i18 strings, fixes #931 (#942 / @arturi)
+- [x] ⚠️ **breaking** informer: make it monochrome and round. always gray, no status colors (#942 / @arturi)
+- [x] url: fix Url plugin reacting to wrong drop/paste events, add ignoreEvent (#942 / @arturi)
+- [x] core: allow editing plugin titles (names) so that e.g. “Camera” can be translated into different languages, fixes #920 (#942 / @arturi)
+- [x] core: remove all: initial — was causing issues when multiple uppy stylesheets are used (#942 / @arturi)
+- [x] provider-views: fix wrong 'no files available' msg flash (#938 / @ifedapoolarewaju)
+- [x] build: Add object rest spread transform (#965 / @goto-bus-stop)
 - [ ] server: bump minor and deprecate that on npm in favour of @uppy/companion
 
 ## 0.26.0

+ 3 - 0
packages/@uppy/aws-s3-multipart/src/index.js

@@ -183,6 +183,9 @@ module.exports = class AwsS3Multipart extends Plugin {
         onPartComplete: (part) => {
           // Store completed parts in state.
           const cFile = this.uppy.getFile(file.id)
+          if (!cFile) {
+            return
+          }
           this.uppy.setFileState(file.id, {
             s3Multipart: Object.assign({}, cFile.s3Multipart, {
               parts: [

+ 8 - 52
packages/@uppy/core/src/_common.scss

@@ -3,11 +3,11 @@
 */
 
 .uppy-Root {
-  all: initial;
   box-sizing: border-box;
   font-family: $font-family-base;
   line-height: 1;
   -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
 }
 
 .uppy-Root *, .uppy-Root *:before, .uppy-Root *:after {
@@ -28,12 +28,9 @@
 .UppyIcon {
   max-width: 100%;
   max-height: 100%;
-  fill: currentColor;
+  fill: currentColor; /* no !important */
   display: inline-block;
-  vertical-align: text-top;
   overflow: hidden;
-  // width: 1em;
-  // height: 1em;
 }
 
 .UppyIcon--svg-baseline {
@@ -41,46 +38,6 @@
   position: relative;
 }
 
-// Buttons
-
-.UppyButton--circular {
-  @include reset-button;
-  box-shadow: 1px 2px 4px 0px rgba($color-black, 0.2);
-  border-radius: 50%;
-  cursor: pointer;
-  transition: all 0.3s;
-}
-
-.UppyButton--blue {
-  color: $color-white;
-  background-color: $color-cornflower-blue;
-
-  &:hover,
-  &:focus {
-    background-color: darken($color-cornflower-blue, 10%);
-  }
-}
-
-.UppyButton--red {
-  color: $color-white;
-  background-color: $color-red;
-
-  &:hover,
-  &:focus {
-    background-color: darken($color-red, 10%);
-  }
-}
-
-.UppyButton--sizeM {
-  width: 60px;
-  height: 60px;
-}
-
-.UppyButton--sizeS {
-  width: 45px;
-  height: 45px;
-}
-
 // Utilities
 
 .uppy-u-reset {
@@ -180,7 +137,7 @@
   padding: 6px 8px;
 }
 
-  .uppy-Dashboard--wide .uppy-c-textInput {
+  .uppy-size--md .uppy-c-textInput {
     font-size: 15px;
     line-height: 1.8;
     padding: 8px 12px;
@@ -203,7 +160,7 @@
   font-size: 16px;
   line-height: 1;
   font-weight: 500;
-  transition: all 0.3s;
+  transition: background-color 0.3s;
   user-select: none;
 }
 
@@ -219,10 +176,9 @@
   color: $color-white;
 }
 
-  .uppy-Dashboard--wide .uppy-c-btn-primary {
+  .uppy-size--md .uppy-c-btn-primary {
     font-size: 15px;
-    padding: 13px 28px;
-    // border-radius: 4px;
+    padding: 13px 22px;
   }
 
   .uppy-c-btn-primary:hover {
@@ -243,7 +199,7 @@
   color: $color-black;
 }
 
-  .uppy-Dashboard--wide .uppy-c-btn-link {
+  .uppy-size--md .uppy-c-btn-link {
     font-size: 15px;
     padding: 13px 28px;
     // border-radius: 4px;
@@ -264,7 +220,7 @@
   border-radius: 2px;
 }
 
-  .uppy-Dashboard--wide .uppy-c-btn--small {
+  .uppy-size--md .uppy-c-btn--small {
     padding: 8px 10px;
     border-radius: 2px;
   }

+ 1 - 1
packages/@uppy/core/src/_variables.scss

@@ -28,4 +28,4 @@ $zIndex-4: 1004 !default;
 $zIndex-5: 1005 !default;
 
 // Media Queries
-$screen-medium: 'only screen and (min-width: 768px)' !default;
+$screen-medium: 'only screen and (min-width: 820px)' !default;

+ 4 - 2
packages/@uppy/dashboard/package.json

@@ -29,9 +29,11 @@
     "@uppy/thumbnail-generator": "0.26.0",
     "@uppy/utils": "0.26.0",
     "classnames": "^2.2.6",
-    "drag-drop": "^2.14.0",
+    "drag-drop": "2.13.3",
     "preact": "^8.2.9",
-    "prettier-bytes": "^1.0.4"
+    "prettier-bytes": "^1.0.4",
+    "preact-css-transition-group": "^1.3.0",
+    "lodash.throttle": "^4.1.1"
   },
   "devDependencies": {
     "@uppy/core": "0.26.0",

+ 0 - 100
packages/@uppy/dashboard/src/FileCard.js

@@ -1,100 +0,0 @@
-const getFileTypeIcon = require('./getFileTypeIcon')
-const FilePreview = require('./FilePreview')
-const { h, Component } = require('preact')
-
-module.exports = class FileCard extends Component {
-  constructor (props) {
-    super(props)
-
-    this.meta = {}
-
-    this.tempStoreMetaOrSubmit = this.tempStoreMetaOrSubmit.bind(this)
-    this.renderMetaFields = this.renderMetaFields.bind(this)
-    this.handleSave = this.handleSave.bind(this)
-    this.handleCancel = this.handleCancel.bind(this)
-  }
-
-  tempStoreMetaOrSubmit (ev) {
-    const file = this.props.files[this.props.fileCardFor]
-
-    if (ev.keyCode === 13) {
-      ev.stopPropagation()
-      ev.preventDefault()
-      this.props.saveFileCard(this.meta, file.id)
-      return
-    }
-
-    const value = ev.target.value
-    const name = ev.target.dataset.name
-    this.meta[name] = value
-  }
-
-  renderMetaFields (file) {
-    const metaFields = this.props.metaFields || []
-    return metaFields.map((field) => {
-      return <fieldset class="uppy-DashboardFileCard-fieldset">
-        <label class="uppy-DashboardFileCard-label">{field.name}</label>
-        <input class="uppy-c-textInput uppy-DashboardFileCard-input"
-          type="text"
-          data-name={field.id}
-          value={file.meta[field.id]}
-          placeholder={field.placeholder}
-          onkeyup={this.tempStoreMetaOrSubmit}
-          onkeydown={this.tempStoreMetaOrSubmit}
-          onkeypress={this.tempStoreMetaOrSubmit} /></fieldset>
-    })
-  }
-
-  handleSave (ev) {
-    const fileID = this.props.fileCardFor
-    this.props.saveFileCard(this.meta, fileID)
-  }
-
-  handleCancel (ev) {
-    this.meta = {}
-    this.props.toggleFileCard()
-  }
-
-  render () {
-    if (!this.props.fileCardFor) {
-      return <div class="uppy-DashboardFileCard" aria-hidden />
-    }
-
-    const file = this.props.files[this.props.fileCardFor]
-
-    return (
-      <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
-        <div style={{ width: '100%', height: '100%' }}>
-          <div class="uppy-DashboardContent-bar">
-            <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
-              {this.props.i18nArray('editing', {
-                file: <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>
-              })}
-            </div>
-            <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
-              onclick={this.handleSave}>{this.props.i18n('done')}</button>
-          </div>
-
-          <div class="uppy-DashboardFileCard-inner">
-            <div class="uppy-DashboardFileCard-preview" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
-              <FilePreview file={file} />
-            </div>
-
-            <div class="uppy-DashboardFileCard-info">
-              {this.renderMetaFields(file)}
-            </div>
-
-            <div class="uppy-Dashboard-actions">
-              <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-actionsBtn"
-                type="button"
-                onclick={this.handleSave}>{this.props.i18n('saveChanges')}</button>
-              <button class="uppy-u-reset uppy-c-btn uppy-c-btn-link uppy-Dashboard-actionsBtn"
-                type="button"
-                onclick={this.handleCancel}>{this.props.i18n('cancel')}</button>
-            </div>
-          </div>
-        </div>
-      </div>
-    )
-  }
-}

+ 0 - 60
packages/@uppy/dashboard/src/FileList.js

@@ -1,60 +0,0 @@
-const FileItem = require('./FileItem')
-const ActionBrowseTagline = require('./ActionBrowseTagline')
-// const { dashboardBgIcon } = require('./icons')
-const classNames = require('classnames')
-const { h } = require('preact')
-
-const poweredByUppy = (props) => {
-  return <a tabindex="-1" href="https://uppy.io" rel="noreferrer noopener" target="_blank" class="uppy-Dashboard-poweredBy">Powered by <svg aria-hidden="true" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11" xmlns="http://www.w3.org/2000/svg">
-    <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fill-rule="evenodd" />
-  </svg><span class="uppy-Dashboard-poweredByUppy">Uppy</span></a>
-}
-
-module.exports = (props) => {
-  const noFiles = props.totalFileCount === 0
-  const dashboardFilesClass = classNames(
-    'uppy-Dashboard-files',
-    { 'uppy-Dashboard-files--noFiles': noFiles }
-  )
-
-  return <ul class={dashboardFilesClass}>
-    {noFiles &&
-      <div class="uppy-Dashboard-bgIcon">
-        <div class="uppy-Dashboard-dropFilesTitle">
-          <ActionBrowseTagline
-            acquirers={props.acquirers}
-            handleInputChange={props.handleInputChange}
-            i18n={props.i18n}
-            i18nArray={props.i18nArray}
-            allowedFileTypes={props.allowedFileTypes}
-            maxNumberOfFiles={props.maxNumberOfFiles}
-          />
-        </div>
-        { props.note && <div class="uppy-Dashboard-note">{props.note}</div> }
-        { props.proudlyDisplayPoweredByUppy && poweredByUppy(props) }
-      </div>
-    }
-    {Object.keys(props.files).map((fileID) => (
-      <FileItem
-        acquirers={props.acquirers}
-        file={props.files[fileID]}
-        toggleFileCard={props.toggleFileCard}
-        showProgressDetails={props.showProgressDetails}
-        info={props.info}
-        log={props.log}
-        i18n={props.i18n}
-        removeFile={props.removeFile}
-        pauseUpload={props.pauseUpload}
-        cancelUpload={props.cancelUpload}
-        retryUpload={props.retryUpload}
-        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
-        hideRetryButton={props.hideRetryButton}
-        resumableUploads={props.resumableUploads}
-        bundled={props.bundled}
-        isWide={props.isWide}
-        showLinkToFileUploadResult={props.showLinkToFileUploadResult}
-        metaFields={props.metaFields}
-      />
-    ))}
-  </ul>
-}

+ 0 - 78
packages/@uppy/dashboard/src/Tabs.js

@@ -1,78 +0,0 @@
-const ActionBrowseTagline = require('./ActionBrowseTagline')
-const { localIcon } = require('./icons')
-const { h, Component } = require('preact')
-
-class Tabs extends Component {
-  constructor (props) {
-    super(props)
-    this.handleClick = this.handleClick.bind(this)
-  }
-
-  handleClick (ev) {
-    this.input.click()
-  }
-
-  render () {
-    const isHidden = Object.keys(this.props.files).length === 0
-    const hasAcquirers = this.props.acquirers.length !== 0
-
-    if (!hasAcquirers) {
-      return (
-        <div class="uppy-DashboardTabs" aria-hidden={isHidden}>
-          <div class="uppy-DashboardTabs-title">
-            <ActionBrowseTagline
-              acquirers={this.props.acquirers}
-              handleInputChange={this.props.handleInputChange}
-              i18n={this.props.i18n}
-              i18nArray={this.props.i18nArray} />
-          </div>
-        </div>
-      )
-    }
-
-    // empty value="" on file input, so that the input is cleared after a file is selected,
-    // because Uppy will be handling the upload and so we can select same file
-    // after removing — otherwise browser thinks it’s already selected
-    return <div class="uppy-DashboardTabs">
-      <ul class="uppy-DashboardTabs-list" role="tablist">
-        <li class="uppy-DashboardTab" role="presentation">
-          <button type="button"
-            class="uppy-DashboardTab-btn"
-            role="tab"
-            tabindex={0}
-            onclick={this.handleClick}>
-            {localIcon()}
-            <div class="uppy-DashboardTab-name">{this.props.i18n('myDevice')}</div>
-          </button>
-          <input class="uppy-Dashboard-input"
-            hidden
-            aria-hidden="true"
-            tabindex={-1}
-            type="file"
-            name="files[]"
-            multiple={this.props.maxNumberOfFiles !== 1}
-            accept={this.props.allowedFileTypes}
-            onchange={this.props.handleInputChange}
-            value=""
-            ref={(input) => { this.input = input }} />
-        </li>
-        {this.props.acquirers.map((target) => {
-          return <li class="uppy-DashboardTab" role="presentation">
-            <button class="uppy-DashboardTab-btn"
-              type="button"
-              role="tab"
-              tabindex={0}
-              aria-controls={`uppy-DashboardContent-panel--${target.id}`}
-              aria-selected={this.props.activePanel.id === target.id}
-              onclick={() => this.props.showPanel(target.id)}>
-              {target.icon()}
-              <div class="uppy-DashboardTab-name">{target.name}</div>
-            </button>
-          </li>
-        })}
-      </ul>
-    </div>
-  }
-}
-
-module.exports = Tabs

+ 2 - 2
packages/@uppy/dashboard/src/ActionBrowseTagline.js → packages/@uppy/dashboard/src/components/ActionBrowseTagline.js

@@ -21,7 +21,7 @@ class ActionBrowseTagline extends Component {
     // because Uppy will be handling the upload and so we can select same file
     // after removing — otherwise browser thinks it’s already selected
     return (
-      <span>
+      <div class="uppy-Dashboard-dropFilesTitle">
         {this.props.acquirers.length === 0
           ? this.props.i18nArray('dropPaste', { browse })
           : this.props.i18nArray('dropPasteImport', { browse })
@@ -39,7 +39,7 @@ class ActionBrowseTagline extends Component {
           ref={(input) => {
             this.input = input
           }} />
-      </span>
+      </div>
     )
   }
 }

+ 101 - 0
packages/@uppy/dashboard/src/components/AddFiles.js

@@ -0,0 +1,101 @@
+const ActionBrowseTagline = require('./ActionBrowseTagline')
+const { localIcon } = require('./icons')
+const { h, Component } = require('preact')
+
+const poweredByUppy = (props) => {
+  return <a tabindex="-1" href="https://uppy.io" rel="noreferrer noopener" target="_blank" class="uppy-Dashboard-poweredBy">Powered by <svg aria-hidden="true" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11" xmlns="http://www.w3.org/2000/svg">
+    <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fill-rule="evenodd" />
+  </svg><span class="uppy-Dashboard-poweredByUppy">Uppy</span></a>
+}
+
+class AddFiles extends Component {
+  constructor (props) {
+    super(props)
+    this.handleClick = this.handleClick.bind(this)
+  }
+
+  handleClick (ev) {
+    this.input.click()
+  }
+
+  render () {
+    // const isHidden = Object.keys(this.props.files).length === 0
+    const hasAcquirers = this.props.acquirers.length !== 0
+
+    if (!hasAcquirers) {
+      return (
+        <div class="uppy-DashboarAddFiles">
+          <div class="uppy-DashboardTabs">
+            <ActionBrowseTagline
+              acquirers={this.props.acquirers}
+              handleInputChange={this.props.handleInputChange}
+              i18n={this.props.i18n}
+              i18nArray={this.props.i18nArray}
+              allowedFileTypes={this.props.allowedFileTypes}
+              maxNumberOfFiles={this.props.maxNumberOfFiles}
+            />
+          </div>
+        </div>
+      )
+    }
+
+    // empty value="" on file input, so that the input is cleared after a file is selected,
+    // because Uppy will be handling the upload and so we can select same file
+    // after removing — otherwise browser thinks it’s already selected
+    return (
+      <div class="uppy-DashboarAddFiles">
+        <div class="uppy-DashboardTabs">
+          <ActionBrowseTagline
+            acquirers={this.props.acquirers}
+            handleInputChange={this.props.handleInputChange}
+            i18n={this.props.i18n}
+            i18nArray={this.props.i18nArray}
+            allowedFileTypes={this.props.allowedFileTypes}
+            maxNumberOfFiles={this.props.maxNumberOfFiles}
+          />
+          <div class="uppy-DashboardTabs-list" role="tablist">
+            <div class="uppy-DashboardTab" role="presentation">
+              <button type="button"
+                class="uppy-DashboardTab-btn"
+                role="tab"
+                tabindex={0}
+                onclick={this.handleClick}>
+                {localIcon()}
+                <div class="uppy-DashboardTab-name">{this.props.i18n('myDevice')}</div>
+              </button>
+              <input class="uppy-Dashboard-input"
+                hidden
+                aria-hidden="true"
+                tabindex={-1}
+                type="file"
+                name="files[]"
+                multiple={this.props.maxNumberOfFiles !== 1}
+                accept={this.props.allowedFileTypes}
+                onchange={this.props.handleInputChange}
+                value=""
+                ref={(input) => { this.input = input }} />
+            </div>
+            {this.props.acquirers.map((target) => {
+              return <div class="uppy-DashboardTab" role="presentation">
+                <button class="uppy-DashboardTab-btn"
+                  type="button"
+                  role="tab"
+                  tabindex={0}
+                  aria-controls={`uppy-DashboardContent-panel--${target.id}`}
+                  aria-selected={this.props.activePanel.id === target.id}
+                  onclick={() => this.props.showPanel(target.id)}>
+                  {target.icon()}
+                  <div class="uppy-DashboardTab-name">{target.name}</div>
+                </button>
+              </div>
+            })}
+          </div>
+        </div>
+        { this.props.note && <div class="uppy-Dashboard-note">{this.props.note}</div> }
+        { this.props.proudlyDisplayPoweredByUppy && poweredByUppy(this.props) }
+      </div>
+    )
+  }
+}
+
+module.exports = AddFiles

+ 21 - 0
packages/@uppy/dashboard/src/components/AddFilesPanel.js

@@ -0,0 +1,21 @@
+const { h } = require('preact')
+const AddFiles = require('./AddFiles')
+
+const AddFilesPanel = (props) => {
+  return (
+    <div class="uppy-Dashboard-AddFilesPanel"
+      aria-hidden={props.showAddFilesPanel}>
+      <div class="uppy-DashboardContent-bar">
+        <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
+          {props.i18n('addingMoreFiles')}
+        </div>
+        <button class="uppy-DashboardContent-back"
+          type="button"
+          onclick={(ev) => props.toggleAddFilesPanel(false)}>{props.i18n('back')}</button>
+      </div>
+      <AddFiles {...props} />
+    </div>
+  )
+}
+
+module.exports = AddFilesPanel

+ 35 - 27
packages/@uppy/dashboard/src/Dashboard.js → packages/@uppy/dashboard/src/components/Dashboard.js

@@ -1,28 +1,23 @@
 const FileList = require('./FileList')
-const Tabs = require('./Tabs')
+const AddFiles = require('./AddFiles')
+const AddFilesPanel = require('./AddFilesPanel')
+const PanelContent = require('./PanelContent')
+const PanelTopBar = require('./PanelTopBar')
 const FileCard = require('./FileCard')
 const classNames = require('classnames')
 const isTouchDevice = require('@uppy/utils/lib/isTouchDevice')
 const { h } = require('preact')
+const PreactCSSTransitionGroup = require('preact-css-transition-group')
 
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
 
-const PanelContent = (props) => {
-  return <div style={{ width: '100%', height: '100%' }}>
-    <div class="uppy-DashboardContent-bar">
-      <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
-        {props.i18n('importFrom', { name: props.activePanel.name })}
-      </div>
-      <button class="uppy-DashboardContent-back"
-        type="button"
-        onclick={props.hideAllPanels}>{props.i18n('done')}</button>
-    </div>
-    {props.getPlugin(props.activePanel.id).render(props.state)}
-  </div>
-}
-
 module.exports = function Dashboard (props) {
+  // if (!props.inline && props.modal.isHidden) {
+  //   return <span />
+  // }
+
+  const noFiles = props.totalFileCount === 0
   const dashboardClassName = classNames(
     { 'uppy-Root': props.isTargetDOMEl },
     'uppy-Dashboard',
@@ -30,7 +25,10 @@ module.exports = function Dashboard (props) {
     { 'uppy-Dashboard--animateOpenClose': props.animateOpenClose },
     { 'uppy-Dashboard--isClosing': props.isClosing },
     { 'uppy-Dashboard--modal': !props.inline },
-    { 'uppy-Dashboard--wide': props.isWide }
+    // { 'uppy-Dashboard--wide': props.isWide },
+    { 'uppy-size--md': props.containerWidth > 576 },
+    { 'uppy-size--lg': props.containerWidth > 700 },
+    { 'uppy-Dashboard--isAddFilesPanelVisible': props.showAddFilesPanel }
   )
 
   return (
@@ -57,20 +55,30 @@ module.exports = function Dashboard (props) {
         </button>
 
         <div class="uppy-Dashboard-innerWrap">
-          <Tabs {...props} />
+          { !noFiles && <PanelTopBar {...props} /> }
 
-          <FileCard {...props} />
+          { noFiles ? <AddFiles {...props} /> : <FileList {...props} /> }
 
-          <div class="uppy-Dashboard-filesContainer">
-            <FileList {...props} />
-          </div>
+          <PreactCSSTransitionGroup
+            transitionName="uppy-transition-slideDownUp"
+            transitionEnterTimeout={250}
+            transitionLeaveTimeout={250}>
+            { props.showAddFilesPanel ? <AddFilesPanel key="AddFilesPanel" {...props} /> : null }
+          </PreactCSSTransitionGroup>
 
-          <div class="uppy-DashboardContent-panel"
-            role="tabpanel"
-            id={props.activePanel && `uppy-DashboardContent-panel--${props.activePanel.id}`}
-            aria-hidden={props.activePanel ? 'false' : 'true'}>
-            {props.activePanel && <PanelContent {...props} />}
-          </div>
+          <PreactCSSTransitionGroup
+            transitionName="uppy-transition-slideDownUp"
+            transitionEnterTimeout={250}
+            transitionLeaveTimeout={250}>
+            { props.fileCardFor ? <FileCard key="FileCard" {...props} /> : null }
+          </PreactCSSTransitionGroup>
+
+          <PreactCSSTransitionGroup
+            transitionName="uppy-transition-slideDownUp"
+            transitionEnterTimeout={250}
+            transitionLeaveTimeout={250}>
+            { props.activePanel ? <PanelContent key="PanelContent" {...props} /> : null }
+          </PreactCSSTransitionGroup>
 
           <div class="uppy-Dashboard-progressindicators">
             {props.progressindicators.map((target) => {

+ 111 - 0
packages/@uppy/dashboard/src/components/FileCard.js

@@ -0,0 +1,111 @@
+const getFileTypeIcon = require('../utils/getFileTypeIcon')
+const FilePreview = require('./FilePreview')
+const ignoreEvent = require('../utils/ignoreEvent.js')
+const { h, Component } = require('preact')
+
+class FileCard extends Component {
+  constructor (props) {
+    super(props)
+
+    this.meta = {}
+
+    this.tempStoreMetaOrSubmit = this.tempStoreMetaOrSubmit.bind(this)
+    this.renderMetaFields = this.renderMetaFields.bind(this)
+    this.handleSave = this.handleSave.bind(this)
+    this.handleCancel = this.handleCancel.bind(this)
+  }
+
+  componentDidMount () {
+    setTimeout(() => {
+      if (!this.firstInput) return
+      this.firstInput.focus({ preventScroll: true })
+    }, 150)
+  }
+
+  tempStoreMetaOrSubmit (ev) {
+    const file = this.props.files[this.props.fileCardFor]
+
+    if (ev.keyCode === 13) {
+      ev.stopPropagation()
+      ev.preventDefault()
+      this.props.saveFileCard(this.meta, file.id)
+      return
+    }
+
+    const value = ev.target.value
+    const name = ev.target.dataset.name
+    this.meta[name] = value
+  }
+
+  renderMetaFields (file) {
+    const metaFields = this.props.metaFields || []
+    return metaFields.map((field, i) => {
+      return <fieldset class="uppy-DashboardFileCard-fieldset">
+        <label class="uppy-DashboardFileCard-label">{field.name}</label>
+        <input class="uppy-c-textInput uppy-DashboardFileCard-input"
+          type="text"
+          data-name={field.id}
+          value={file.meta[field.id]}
+          placeholder={field.placeholder}
+          onkeyup={this.tempStoreMetaOrSubmit}
+          onkeydown={this.tempStoreMetaOrSubmit}
+          onkeypress={this.tempStoreMetaOrSubmit}
+          ref={(el) => {
+            if (i === 0) this.firstInput = el
+          }} /></fieldset>
+    })
+  }
+
+  handleSave (ev) {
+    const fileID = this.props.fileCardFor
+    this.props.saveFileCard(this.meta, fileID)
+  }
+
+  handleCancel (ev) {
+    this.meta = {}
+    this.props.toggleFileCard()
+  }
+
+  render () {
+    const file = this.props.files[this.props.fileCardFor]
+
+    return (
+      <div class="uppy-DashboardFileCard"
+        onDragOver={ignoreEvent}
+        onDragLeave={ignoreEvent}
+        onDrop={ignoreEvent}
+        onPaste={ignoreEvent}>
+        <div class="uppy-DashboardContent-bar">
+          <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
+            {this.props.i18nArray('editing', {
+              file: <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>
+            })}
+          </div>
+          <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
+            onclick={this.handleSave}>{this.props.i18n('done')}</button>
+        </div>
+
+        <div class="uppy-DashboardFileCard-inner">
+          <div class="uppy-DashboardFileCard-preview" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
+            <FilePreview file={file} />
+          </div>
+
+          <div class="uppy-DashboardFileCard-info">
+            {this.renderMetaFields(file)}
+          </div>
+
+          <div class="uppy-Dashboard-actions">
+            <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-actionsBtn"
+              type="button"
+              onclick={this.handleSave}>{this.props.i18n('saveChanges')}</button>
+            <button class="uppy-u-reset uppy-c-btn uppy-c-btn-link uppy-Dashboard-actionsBtn"
+              type="button"
+              onclick={this.handleCancel}>{this.props.i18n('cancel')}</button>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+module.exports = FileCard

+ 31 - 31
packages/@uppy/dashboard/src/FileItem.js → packages/@uppy/dashboard/src/components/FileItem.js

@@ -1,11 +1,11 @@
 const getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension')
-const truncateString = require('./truncateString')
-const copyToClipboard = require('./copyToClipboard')
+const truncateString = require('../utils/truncateString')
+const copyToClipboard = require('../utils/copyToClipboard')
 const prettyBytes = require('prettier-bytes')
 const FileItemProgress = require('./FileItemProgress')
-const getFileTypeIcon = require('./getFileTypeIcon')
+const getFileTypeIcon = require('../utils/getFileTypeIcon')
 const FilePreview = require('./FilePreview')
-const { iconEdit, iconCopy, iconRetry } = require('./icons')
+const { iconCopy, iconRetry } = require('./icons')
 const classNames = require('classnames')
 const { h } = require('preact')
 
@@ -56,7 +56,7 @@ module.exports = function fileItem (props) {
   const error = file.error || false
 
   const fileName = getFileNameAndExtension(file.meta.name).name
-  const truncatedFileName = props.isWide ? truncateString(fileName, 14) : fileName
+  const truncatedFileName = props.isWide ? truncateString(fileName, 30) : fileName
 
   const onPauseResumeCancelRetry = (ev) => {
     if (isUploaded) return
@@ -127,7 +127,7 @@ module.exports = function fileItem (props) {
       </div>
       <div class="uppy-DashboardItem-status">
         {file.data.size ? <div class="uppy-DashboardItem-statusSize">{prettyBytes(file.data.size)}</div> : null}
-        {file.source && <div class="uppy-DashboardItem-sourceIcon">
+        {(file.source && file.source !== props.id) && <div class="uppy-DashboardItem-sourceIcon">
             {acquirers.map(acquirer => {
               if (acquirer.id === file.source) {
                 return <span title={props.i18n('fileSource', { name: acquirer.name })}>
@@ -137,32 +137,32 @@ module.exports = function fileItem (props) {
             })}
           </div>
         }
+        {(!uploadInProgressOrComplete && props.metaFields && props.metaFields.length)
+          ? <button class="uppy-DashboardItem-edit"
+            type="button"
+            aria-label={props.i18n('editFile')}
+            title={props.i18n('editFile')}
+            onclick={(e) => props.toggleFileCard(file.id)}>
+            {props.i18n('edit')}
+          </button>
+          : null
+        }
+        {props.showLinkToFileUploadResult && file.uploadURL
+          ? <button class="uppy-DashboardItem-copyLink"
+            type="button"
+            aria-label={props.i18n('copyLink')}
+            title={props.i18n('copyLink')}
+            onclick={() => {
+              copyToClipboard(file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
+                .then(() => {
+                  props.log('Link copied to clipboard.')
+                  props.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
+                })
+                .catch(props.log)
+            }}>{iconCopy()}</button>
+          : ''
+        }
       </div>
-      {(!uploadInProgressOrComplete && props.metaFields && props.metaFields.length)
-        ? <button class="uppy-DashboardItem-edit"
-          type="button"
-          aria-label={props.i18n('editFile')}
-          title={props.i18n('editFile')}
-          onclick={(e) => props.toggleFileCard(file.id)}>
-          {iconEdit()}
-        </button>
-        : null
-      }
-      {props.showLinkToFileUploadResult && file.uploadURL
-        ? <button class="uppy-DashboardItem-copyLink"
-          type="button"
-          aria-label={props.i18n('copyLink')}
-          title={props.i18n('copyLink')}
-          onclick={() => {
-            copyToClipboard(file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
-              .then(() => {
-                props.log('Link copied to clipboard.')
-                props.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
-              })
-              .catch(props.log)
-          }}>{iconCopy()}</button>
-        : ''
-      }
     </div>
     <div class="uppy-DashboardItem-action">
       {!isUploaded &&

+ 0 - 0
packages/@uppy/dashboard/src/FileItemProgress.js → packages/@uppy/dashboard/src/components/FileItemProgress.js


+ 23 - 0
packages/@uppy/dashboard/src/components/FileList.js

@@ -0,0 +1,23 @@
+const FileItem = require('./FileItem')
+const classNames = require('classnames')
+const { h } = require('preact')
+
+module.exports = (props) => {
+  const noFiles = props.totalFileCount === 0
+  const dashboardFilesClass = classNames(
+    'uppy-Dashboard-files',
+    { 'uppy-Dashboard-files--noFiles': noFiles }
+  )
+
+  return (
+    <ul class={dashboardFilesClass}>
+      {Object.keys(props.files).map((fileID) => (
+        <FileItem
+          {...props}
+          acquirers={props.acquirers}
+          file={props.files[fileID]}
+        />
+      ))}
+    </ul>
+  )
+}

+ 1 - 1
packages/@uppy/dashboard/src/FilePreview.js → packages/@uppy/dashboard/src/components/FilePreview.js

@@ -1,4 +1,4 @@
-const getFileTypeIcon = require('./getFileTypeIcon')
+const getFileTypeIcon = require('../utils/getFileTypeIcon')
 const { h } = require('preact')
 
 module.exports = function FilePreview (props) {

+ 28 - 0
packages/@uppy/dashboard/src/components/PanelContent.js

@@ -0,0 +1,28 @@
+const { h } = require('preact')
+const ignoreEvent = require('../utils/ignoreEvent.js')
+
+function PanelContent (props) {
+  return (
+    <div class="uppy-DashboardContent-panel"
+      role="tabpanel"
+      id={props.activePanel && `uppy-DashboardContent-panel--${props.activePanel.id}`}
+      onDragOver={ignoreEvent}
+      onDragLeave={ignoreEvent}
+      onDrop={ignoreEvent}
+      onPaste={ignoreEvent}>
+      <div class="uppy-DashboardContent-bar">
+        <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
+          {props.i18n('importFrom', { name: props.activePanel.name })}
+        </div>
+        <button class="uppy-DashboardContent-back"
+          type="button"
+          onclick={props.hideAllPanels}>{props.i18n('done')}</button>
+      </div>
+      <div class="uppy-DashboardContent-panelBody">
+        {props.getPlugin(props.activePanel.id).render(props.state)}
+      </div>
+    </div>
+  )
+}
+
+module.exports = PanelContent

+ 31 - 0
packages/@uppy/dashboard/src/components/PanelTopBar.js

@@ -0,0 +1,31 @@
+const { h } = require('preact')
+
+function DashboardContentTitle (props) {
+  if (props.newFiles.length) {
+    return props.i18n('xFilesSelected', { smart_count: props.newFiles.length })
+  }
+}
+
+function PanelTopBar (props) {
+  return (
+    <div class="uppy-DashboardContent-bar">
+      <button class="uppy-DashboardContent-back"
+        type="button"
+        onclick={props.cancelAll}>{props.i18n('cancel')}</button>
+      <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
+        <DashboardContentTitle {...props} />
+      </div>
+      <button class="uppy-DashboardContent-addMore"
+        type="button"
+        aria-label={props.i18n('addMoreFiles')}
+        title={props.i18n('addMoreFiles')}
+        onclick={() => props.toggleAddFilesPanel(true)}>
+        <svg class="UppyIcon" width="15" height="15" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg">
+          <path d="M7,6 L13,6 L13,7 L7,7 L7,13 L6,13 L6,7 L0,7 L0,6 L6,6 L6,0 L7,0 L7,6 Z" />
+        </svg>
+      </button>
+    </div>
+  )
+}
+
+module.exports = PanelTopBar

+ 4 - 18
packages/@uppy/dashboard/src/icons.js → packages/@uppy/dashboard/src/components/icons.js

@@ -3,7 +3,7 @@ const { h } = require('preact')
 // https://css-tricks.com/creating-svg-icon-system-react/
 
 function defaultTabIcon () {
-  return <svg aria-hidden="true" class="UppyIcon" width="30" height="30" viewBox="0 0 30 30">
+  return <svg aria-hidden="true" width="30" height="30" viewBox="0 0 30 30">
     <path d="M15 30c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C6.716 0 0 6.716 0 15c0 8.284 6.716 15 15 15zm4.258-12.676v6.846h-8.426v-6.846H5.204l9.82-12.364 9.82 12.364H19.26z" />
   </svg>
 }
@@ -30,21 +30,15 @@ function iconPause () {
   </svg>
 }
 
-function iconEdit () {
-  return <svg aria-hidden="true" class="UppyIcon" width="28" height="28" viewBox="0 0 28 28">
-    <path d="M25.436 2.566a7.98 7.98 0 0 0-2.078-1.51C22.638.703 21.906.5 21.198.5a3 3 0 0 0-1.023.17 2.436 2.436 0 0 0-.893.562L2.292 18.217.5 27.5l9.28-1.796 16.99-16.99c.255-.254.444-.56.562-.888a3 3 0 0 0 .17-1.023c0-.708-.205-1.44-.555-2.16a8 8 0 0 0-1.51-2.077zM9.01 24.252l-4.313.834c0-.03.008-.06.012-.09.007-.944-.74-1.715-1.67-1.723-.04 0-.078.007-.118.01l.83-4.29L17.72 5.024l5.264 5.264L9.01 24.252zm16.84-16.96a.818.818 0 0 1-.194.31l-1.57 1.57-5.26-5.26 1.57-1.57a.82.82 0 0 1 .31-.194 1.45 1.45 0 0 1 .492-.074c.397 0 .917.126 1.468.397.55.27 1.13.678 1.656 1.21.53.53.94 1.11 1.208 1.655.272.55.397 1.07.393 1.468.004.193-.027.358-.074.488z" />
-  </svg>
-}
-
 function localIcon () {
-  return <svg aria-hidden="true" class="UppyIcon" width="27" height="25" viewBox="0 0 27 25">
+  return <svg aria-hidden="true" fill="#607d8b" width="27" height="25" viewBox="0 0 27 25">
     <path d="M5.586 9.288a.313.313 0 0 0 .282.176h4.84v3.922c0 1.514 1.25 2.24 2.792 2.24 1.54 0 2.79-.726 2.79-2.24V9.464h4.84c.122 0 .23-.068.284-.176a.304.304 0 0 0-.046-.324L13.735.106a.316.316 0 0 0-.472 0l-7.63 8.857a.302.302 0 0 0-.047.325z" />
     <path d="M24.3 5.093c-.218-.76-.54-1.187-1.208-1.187h-4.856l1.018 1.18h3.948l2.043 11.038h-7.193v2.728H9.114v-2.725h-7.36l2.66-11.04h3.33l1.018-1.18H3.907c-.668 0-1.06.46-1.21 1.186L0 16.456v7.062C0 24.338.676 25 1.51 25h23.98c.833 0 1.51-.663 1.51-1.482v-7.062L24.3 5.093z" />
   </svg>
 }
 
 function iconRetry () {
-  return <svg class="UppyIcon retry" width="28" height="31" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
+  return <svg aria-hidden="true" class="UppyIcon retry" width="28" height="31" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
     <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
     <path d="M7.9 3H10v2H7.9z" />
     <path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z" />
@@ -88,25 +82,17 @@ function iconText () {
   </svg>
 }
 
-function dashboardBgIcon () {
-  return <svg aria-hidden="true" class="UppyIcon" width="48" height="69" viewBox="0 0 48 69">
-    <path d="M.5 1.5h5zM10.5 1.5h5zM20.5 1.5h5zM30.504 1.5h5zM45.5 11.5v5zM45.5 21.5v5zM45.5 31.5v5zM45.5 41.502v5zM45.5 51.502v5zM45.5 61.5v5zM45.5 66.502h-4.998zM35.503 66.502h-5zM25.5 66.502h-5zM15.5 66.502h-5zM5.5 66.502h-5zM.5 66.502v-5zM.5 56.502v-5zM.5 46.503V41.5zM.5 36.5v-5zM.5 26.5v-5zM.5 16.5v-5zM.5 6.5V1.498zM44.807 11H36V2.195z" />
-  </svg>
-}
-
 module.exports = {
   defaultTabIcon,
   iconCopy,
   iconResume,
   iconPause,
   iconRetry,
-  iconEdit,
   localIcon,
   checkIcon,
   iconAudio,
   iconVideo,
   iconPDF,
   iconFile,
-  iconText,
-  dashboardBgIcon
+  iconText
 }

+ 47 - 9
packages/@uppy/dashboard/src/index.js

@@ -1,14 +1,15 @@
 const { Plugin } = require('@uppy/core')
 const Translator = require('@uppy/utils/lib/Translator')
 const dragDrop = require('drag-drop')
-const DashboardUI = require('./Dashboard')
+const DashboardUI = require('./components/Dashboard')
 const StatusBar = require('@uppy/status-bar')
 const Informer = require('@uppy/informer')
 const ThumbnailGenerator = require('@uppy/thumbnail-generator')
 const findAllDOMElements = require('@uppy/utils/lib/findAllDOMElements')
 const toArray = require('@uppy/utils/lib/toArray')
 const prettyBytes = require('prettier-bytes')
-const { defaultTabIcon } = require('./icons')
+const throttle = require('lodash.throttle')
+const { defaultTabIcon } = require('./components/icons')
 
 // Some code for managing focus was adopted from https://github.com/ghosh/micromodal
 // MIT licence, https://github.com/ghosh/micromodal/blob/master/LICENSE.md
@@ -47,6 +48,8 @@ module.exports = class Dashboard extends Plugin {
         closeModal: 'Close Modal',
         upload: 'Upload',
         importFrom: 'Import from %{name}',
+        addingMoreFiles: 'Adding more files',
+        addMoreFiles: 'Add more files',
         dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
         dashboardTitle: 'Uppy Dashboard',
         copyLinkToClipboardSuccess: 'Link copied to clipboard',
@@ -54,16 +57,18 @@ module.exports = class Dashboard extends Plugin {
         copyLink: 'Copy link',
         fileSource: 'File source: %{name}',
         done: 'Done',
+        back: 'Back',
         name: 'Name',
         removeFile: 'Remove file',
         editFile: 'Edit file',
         editing: 'Editing %{file}',
+        edit: 'Edit',
         finishEditingFile: 'Finish editing file',
         saveChanges: 'Save changes',
         cancel: 'Cancel',
         localDisk: 'Local Disk',
         myDevice: 'My Device',
-        dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}',
+        dropPasteImport: 'Drop files here, paste, %{browse} or import from',
         dropPaste: 'Drop files here, paste or %{browse}',
         browse: 'browse',
         fileProgress: 'File progress: upload speed and ETA',
@@ -74,6 +79,10 @@ module.exports = class Dashboard extends Plugin {
         resumeUpload: 'Resume upload',
         pauseUpload: 'Pause upload',
         retryUpload: 'Retry upload',
+        xFilesSelected: {
+          0: '%{smart_count} file selected',
+          1: '%{smart_count} files selected'
+        },
         uploadXFiles: {
           0: 'Upload %{smart_count} file',
           1: 'Upload %{smart_count} files'
@@ -146,10 +155,12 @@ module.exports = class Dashboard extends Plugin {
     this.onKeydown = this.onKeydown.bind(this)
     this.handleClickOutside = this.handleClickOutside.bind(this)
     this.toggleFileCard = this.toggleFileCard.bind(this)
+    this.toggleAddFilesPanel = this.toggleAddFilesPanel.bind(this)
     this.handleDrop = this.handleDrop.bind(this)
     this.handlePaste = this.handlePaste.bind(this)
     this.handleInputChange = this.handleInputChange.bind(this)
     this.updateDashboardElWidth = this.updateDashboardElWidth.bind(this)
+    this.throttledUpdateDashboardElWidth = throttle(this.updateDashboardElWidth, 500, { leading: true, trailing: true })
     this.render = this.render.bind(this)
     this.install = this.install.bind(this)
   }
@@ -196,7 +207,8 @@ module.exports = class Dashboard extends Plugin {
 
   hideAllPanels () {
     this.setPluginState({
-      activePanel: false
+      activePanel: false,
+      showAddFilesPanel: false
     })
   }
 
@@ -222,6 +234,7 @@ module.exports = class Dashboard extends Plugin {
 
   getFocusableNodes () {
     const nodes = this.el.querySelectorAll(FOCUSABLE_ELEMENTS)
+    console.log(Object.keys(nodes).map((key) => nodes[key]))
     return Object.keys(nodes).map((key) => nodes[key])
   }
 
@@ -276,10 +289,6 @@ module.exports = class Dashboard extends Plugin {
   }
 
   openModal () {
-    this.setPluginState({
-      isHidden: false
-    })
-
     // save scroll position
     this.savedScrollPosition = window.scrollY
     // save active element, so we can restore focus when modal is closed
@@ -289,6 +298,20 @@ module.exports = class Dashboard extends Plugin {
       document.body.classList.add('uppy-Dashboard-isFixed')
     }
 
+    if (this.opts.animateOpenClose && this.getPluginState().isClosing) {
+      const handler = () => {
+        this.setPluginState({
+          isHidden: false
+        })
+        this.el.removeEventListener('animationend', handler, false)
+      }
+      this.el.addEventListener('animationend', handler, false)
+    } else {
+      this.setPluginState({
+        isHidden: false
+      })
+    }
+
     if (this.opts.browserBackButtonClose) {
       this.updateBrowserHistory()
     }
@@ -419,9 +442,10 @@ module.exports = class Dashboard extends Plugin {
     })
 
     this.updateDashboardElWidth()
-    window.addEventListener('resize', this.updateDashboardElWidth)
+    window.addEventListener('resize', this.throttledUpdateDashboardElWidth)
 
     this.uppy.on('plugin-remove', this.removeTarget)
+    this.uppy.on('file-added', (ev) => this.toggleAddFilesPanel(false))
   }
 
   removeEvents () {
@@ -434,10 +458,13 @@ module.exports = class Dashboard extends Plugin {
     window.removeEventListener('resize', this.updateDashboardElWidth)
     window.removeEventListener('popstate', this.handlePopState, false)
     this.uppy.off('plugin-remove', this.removeTarget)
+    this.uppy.off('file-added', (ev) => this.toggleAddFilesPanel(false))
   }
 
   updateDashboardElWidth () {
     const dashboardEl = this.el.querySelector('.uppy-Dashboard-inner')
+    if (!dashboardEl) return
+
     this.uppy.log(`Dashboard width: ${dashboardEl.offsetWidth}`)
 
     this.setPluginState({
@@ -451,6 +478,12 @@ module.exports = class Dashboard extends Plugin {
     })
   }
 
+  toggleAddFilesPanel (show) {
+    this.setPluginState({
+      showAddFilesPanel: show
+    })
+  }
+
   handleDrop (files) {
     this.uppy.log('[Dashboard] Files were dropped')
 
@@ -576,8 +609,11 @@ module.exports = class Dashboard extends Plugin {
       pauseUpload: this.uppy.pauseResume,
       retryUpload: this.uppy.retryUpload,
       cancelUpload: cancelUpload,
+      cancelAll: this.uppy.cancelAll,
       fileCardFor: pluginState.fileCardFor,
       toggleFileCard: this.toggleFileCard,
+      toggleAddFilesPanel: this.toggleAddFilesPanel,
+      showAddFilesPanel: pluginState.showAddFilesPanel,
       saveFileCard: saveFileCard,
       updateDashboardElWidth: this.updateDashboardElWidth,
       width: this.opts.width,
@@ -586,6 +622,7 @@ module.exports = class Dashboard extends Plugin {
       proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
       currentWidth: pluginState.containerWidth,
       isWide: pluginState.containerWidth > 400,
+      containerWidth: pluginState.containerWidth,
       isTargetDOMEl: this.isTargetDOMEl,
       allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
       maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles
@@ -605,6 +642,7 @@ module.exports = class Dashboard extends Plugin {
     this.setPluginState({
       isHidden: true,
       showFileCard: false,
+      showAddFilesPanel: false,
       activePanel: false,
       metaFields: this.opts.metaFields,
       targets: []

+ 274 - 207
packages/@uppy/dashboard/src/style.scss

@@ -3,6 +3,32 @@
 @import '@uppy/status-bar/src/style.scss';
 @import '@uppy/provider-views/src/style.scss';
 
+// transitions //
+
+.uppy-transition-slideDownUp-enter {
+  opacity: 0.01;
+  transform: translate3d(0, -105%, 0);
+  transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out;
+}
+
+.uppy-transition-slideDownUp-enter.uppy-transition-slideDownUp-enter-active {
+  opacity: 1;
+  transform: translate3d(0, 0, 0);
+}
+
+.uppy-transition-slideDownUp-leave {
+  opacity: 1;
+  transform: translate3d(0, 0, 0);
+  transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out;
+}
+
+.uppy-transition-slideDownUp-leave.uppy-transition-slideDownUp-leave-active {
+  opacity: 0.01;
+  transform: translate3d(0, -105%, 0);
+}
+
+// end transitions //
+
 .uppy-Dashboard--modal {
   z-index: $zIndex-2;
 }
@@ -85,15 +111,14 @@
 
 .uppy-Dashboard-inner {
   position: relative;
-  background-color: darken($color-white, 2%);
+  background-color: $color-almost-white;
   max-width: 100%; /* no !important */
   max-height: 100%; /* no !important */
-  width: 100%; /* no !important */
-  height: 100%; /* no !important */
-  min-width: 300px;
+  min-width: 290px;
   min-height: 400px;
   outline: none;
   border: 1px solid rgba($color-gray, 0.2);
+  border-radius: 5px;
 
   .uppy-Dashboard--modal & {
     z-index: $zIndex-3;
@@ -102,7 +127,6 @@
   @media #{$screen-medium} {
     width: 750px; /* no !important */
     height: 550px; /* no !important */
-    border-radius: 5px;
   }
 }
 
@@ -111,18 +135,16 @@
   flex-direction: column;
   height: 100%;
   overflow: hidden;
-  min-height: 300px;
   position: relative;
-
-  @media #{$screen-medium} {
-    border-radius: 5px;
-  }
+  border-radius: 5px;
 }
 
 .uppy-Dashboard--modal .uppy-Dashboard-inner {
   position: fixed;
-  top: 0;
-  left: 0;
+  top: 35px;
+  left: 15px;
+  right: 15px;
+  bottom: 15px;
   border: none;
 
   @media #{$screen-medium} {
@@ -137,17 +159,16 @@
   @include reset-button;
   display: none;
   position: absolute;
-  top: 2px;
-  right: 8px;
+  top: -33px;
+  right: -2px;
   cursor: pointer;
-  color: rgba($color-asphalt-gray, 0.5);
-  transition: all 0.3s;
-  font-size: 23px;
+  color: rgba($color-white, 0.9);
+  font-size: 27px;
 
-  .uppy-Dashboard--wide & {
-    font-size: 30px;
-    top: 2px;
-    right: 8px;
+  @media #{$screen-medium} {
+    font-size: 35px;
+    top: -10px;
+    right: -35px;
   }
 
   .uppy-Dashboard--modal & {
@@ -156,23 +177,26 @@
   }
 }
 
-.uppy-Dashboard-close:hover {
-  color: $color-cornflower-blue;
+.uppy-DashboarAddFiles {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  height: 100%;
+  position: relative;
+  text-align: center;
+  flex: 1;
 }
 
-
 .uppy-DashboardTabs {
-  padding: 7px;
-  // padding-right: 28px;
-  border-bottom: 1px solid rgba($color-gray, 0.3);
-  overflow-x: auto;
-  -webkit-overflow-scrolling: touch;
-  // overflow-x: auto;
-  // -webkit-overflow-scrolling: touch;
-}
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  width: 100%;
 
-.uppy-DashboardTabs[aria-hidden=true] {
-  display: none;
+  .uppy-size--md & {
+    align-items: center;
+  }
 }
 
 .uppy-DashboardTabs-title {
@@ -184,7 +208,7 @@
   text-align: center;
   color: $color-asphalt-gray;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     font-size: 17px;
     line-height: 40px;
   }
@@ -202,70 +226,111 @@
   }
 
 .uppy-DashboardTabs-list {
-  list-style-type: none;
-  margin: 0;
-  padding: 0;
-  // display: flex;
-  // justify-content: center;
-  // align-items: center;
-  white-space: nowrap;
-  text-align: center;
+  display: flex;
+  flex-direction: column;
+  max-height: 300px;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+  margin-top: 10px;
+
+  .uppy-size--md & {
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: center;
+    max-width: 600px;
+    overflow-x: initial;
+    margin-top: 30px;
+  }
 }
 
 .uppy-DashboardTab {
-  width: 70px;
-  margin: 0;
+  width: 100%;
   display: inline-block;
   text-align: center;
+  border-bottom: 1px solid rgba($color-gray, 0.2);
 
-  .uppy-Dashboard--wide & {
-    width: 75px;
-    margin: 0 5px;
+  .uppy-size--md & {
+    width: initial;
+    margin-bottom: 20px;
+    border-bottom: initial;
   }
 }
 
 .uppy-DashboardTab-btn {
   width: 100%;
+  height: 100%;
   cursor: pointer;
   border: 0;
   background-color: transparent;
   -webkit-appearance: none;
   appearance: none;
-  // outline: none;
-  transition: all 0.3s;
   color: darken($color-gray, 25%);
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 14px 20px;
+  line-height: 1;
+
+  .uppy-size--md & {
+    width: 90px;
+    margin: 0 5px;
+    flex-direction: column;
+    padding: 0;
+  }
 }
 
-  // .uppy-DashboardTab-btn:focus,
-  // .uppy-DashboardTab-btn:active,
   .uppy-DashboardTab-btn:hover {
     color: $color-cornflower-blue;
   }
 
+  .uppy-DashboardTab-btn svg {
+    margin-right: 10px;
+
+    .uppy-size--md & {
+      margin-right: 0;
+    }
+  }
+
+  .uppy-DashboardTab-btn svg,
+  .uppy-DashboardTab-btn svg * {
+    max-width: 100%;
+    max-height: 100%;
+    display: inline-block;
+    vertical-align: text-top;
+    overflow: hidden;
+    transition: transform 0.2s;
+    will-change: transform;
+  }
+
+  .uppy-DashboardTab-btn:hover svg {
+    transform: scale(1.1, 1.1);
+  }
+
 .uppy-DashboardTab-name {
-  font-size: 8px;
-  line-height: 11px;
-  margin-top: 5px;
-  margin-bottom: 0;
+  font-size: 14px;
   font-weight: 500;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  // line-height: 14px;
+  // overflow: hidden;
+  // text-overflow: ellipsis;
+  // white-space: nowrap;
 
-  .uppy-Dashboard--wide & {
-    font-size: 9px;
+  .uppy-size--md & {
+    font-size: 11px;
+    line-height: 14px;
+    margin-top: 8px;
+    margin-bottom: 0;
   }
 }
 
 // On SVG sizing: https://sarasoueidan.com/blog/svg-style-inheritance-and-FOUSVG/
-.uppy-DashboardTab .UppyIcon {
+.uppy-DashboardTab svg {
   width: 18px;
   height: 18px;
   vertical-align: middle;
 
-  .uppy-Dashboard--wide & {
-    width: 23px;
-    height: 23px;
+  .uppy-size--md & {
+    width: 27px;
+    height: 27px;
   }
 }
 
@@ -279,20 +344,20 @@
 }
 
 .uppy-DashboardContent-bar {
-  position: absolute;
-  top: 0;
-  left: 0;
   display: flex;
   align-items: center;
+  justify-content: space-between;
   height: 40px;
   width: 100%;
   border-bottom: 1px solid rgba($color-gray, 0.3);
+  
   z-index: $zIndex-4;
-  background-color: darken($color-white, 4%);
-  padding: 0 15px;
+  background-color: $color-almost-white;
+  padding: 0 10px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     height: 50px;
+    padding: 0 15px;
   }
 }
 
@@ -302,7 +367,7 @@
   left: 0;
   right: 0;
   text-align: center;
-  font-size: 14px;
+  font-size: 12px;
   line-height: 40px;
   font-weight: normal;
   max-width: 170px;
@@ -311,8 +376,8 @@
   overflow-x: hidden;
   margin: auto;
 
-  .uppy-Dashboard--wide & {
-    font-size: 16px;
+  .uppy-size--md & {
+    font-size: 14px;
     line-height: 50px;
     max-width: 300px;
   }
@@ -320,38 +385,75 @@
 
 .uppy-DashboardContent-back {
   @include reset-button;
-  font-size: 14px;
+  font-size: 13px;
   font-weight: 500;
   cursor: pointer;
   color: $color-cornflower-blue;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     font-size: 15px;
   }
 }
 
+.uppy-DashboardContent-addMore {
+  @include reset-button;
+  font-weight: 500;
+  cursor: pointer;
+  color: $color-cornflower-blue;
+  stroke: $color-cornflower-blue;
+  stroke-width: 0.7px;
+  width: 13px;
+  height: 13px;
+
+  .uppy-size--md & {
+    width: 15px;
+    height: 15px;
+  }
+}
+
+  .uppy-DashboardContent-addMore svg {
+    vertical-align: text-top;
+  }
+
 .uppy-DashboardContent-panel {
   position: absolute;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
-  transform: translate3d(0, -105%, 0);
-  transition: transform 0.2s ease-in-out;
   background-color: darken($color-white, 4%);
+  overflow: hidden;
+  z-index: $zIndex-5;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+
+.uppy-Dashboard-AddFilesPanel {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: $color-almost-white;
+  background: linear-gradient(0deg, $color-almost-white 35%, rgba($color-almost-white, 0.85) 100%);
   box-shadow: 0 0 10px 5px rgba($color-black, 0.15);
-  padding-top: 40px;
   overflow: hidden;
-  z-index: $zIndex-4;
+  z-index: $zIndex-5;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: column;
+}
 
-  .uppy-Dashboard--wide & {
-    padding-top: 50px;
+  .uppy-Dashboard--isAddFilesPanelVisible .uppy-Dashboard-files {
+    filter: blur(2px);
   }
-}
 
-.uppy-DashboardContent-panel[aria-hidden=false] {
-  transform: translate3d(0, 0, 0);
-}
+// .uppy-Dashboard-AddFilesPanel[aria-hidden=true],
+// .uppy-DashboardContent-panel[aria-hidden=true] {
+//   transform: translate3d(0, 0, 0);
+// }
 
 // Progress bar placeholder
 
@@ -417,49 +519,40 @@
   padding: 0 0 10px 0;
   overflow-y: auto;
   -webkit-overflow-scrolling: touch;
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
+  flex: 1;
 }
 
-  .uppy-Dashboard--wide .uppy-Dashboard-files {
-    padding: 15px 10px 10px 10px;
+  .uppy-size--md .uppy-Dashboard-files {
+    padding-top: 10px;
   }
 
-.uppy-Dashboard.drag .uppy-Dashboard-innerWrap  {
-  background-color: darken($color-white, 20%)
+.uppy-Dashboard.drag .uppy-Dashboard-innerWrap {
+  background-color: darken($color-almost-white, 25%)
 }
 
-.uppy-Dashboard.drag .uppy-Dashboard-files--noFiles {
-  border-color: darken($color-white, 20%);
+.uppy-Dashboard.drag .uppy-Dashboard-AddFilesPanel {
+  background: darken($color-almost-white, 20%)
 }
 
-.uppy-Dashboard-bgIcon {
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.uppy-Dashboard.drag .uppy-Dashboard-bgIcon {
-  opacity: 1;
+.uppy-Dashboard.drag .uppy-Dashboard-files--noFiles {
+  border-color: darken($color-almost-white, 20%);
 }
 
 .uppy-Dashboard-dropFilesTitle {
-  max-width: 460px;
+  max-width: 300px;
   text-align: center;
-  font-size: 18px;
+  font-size: 16px;
   line-height: 1.45;
   font-weight: 400;
-  color: rgba($color-asphalt-gray, 0.8);
+  color: $color-asphalt-gray;
+  margin: auto;
+  // margin-bottom: 10px;
   padding: 0 15px;
-  // margin: 0;
-  // margin-top: 25px;
 
-  .uppy-Dashboard--wide & {
-    font-size: 24px;
+  .uppy-size--md & {
+    max-width: 400px;
+    font-size: 27px;
+    // margin-bottom: 30px;
   }
 }
 
@@ -473,35 +566,27 @@
   left: 0;
   width: 100%;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     font-size: 16px;
   }
 }
 
 .uppy-Dashboard-poweredBy {
-  width: 100%;
+  // width: 100%;
   text-align: center;
   position: absolute;
   bottom: 23px;
   font-size: 11px;
   color: $color-gray;
   text-decoration: none;
-  padding-top: 8px;
+  margin-top: 8px;
   padding-right: 2px;
 }
 
-  // .uppy-Dashboard--modal .uppy-Dashboard-poweredBy {
-  //   color: rgba($color-white, 0.7);
-  // }
-
 .uppy-Dashboard-poweredByUppy {
   color: $color-gray;
 }
 
-  // .uppy-Dashboard--modal .uppy-Dashboard-poweredByUppy {
-  //   color: $color-white;
-  // }
-
 .uppy-Dashboard-poweredByIcon {
   stroke: $color-gray;
   fill: none;
@@ -512,28 +597,22 @@
   opacity: 0.9;
 }
 
-  // .uppy-Dashboard--modal .uppy-Dashboard-poweredByIcon {
-  //   stroke: transparent;
-  //   fill: $color-uppy-pink;
-  // }
-
 .uppy-DashboardItem {
   list-style: none;
   margin: 10px 0;
   position: relative;
-  // background-color: $color-white;
   display: flex;
   align-items: center;
   border-bottom: 1px solid lighten($color-gray, 35%);
   padding-bottom: 10px;
   padding-left: 10px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     flex-direction: column;
     float: left;
     width: 140px;
     height: 170px;
-    margin: 5px 15px;
+    margin: 5px 20px;
     border: 0;
     background-color: initial;
     border-bottom: none;
@@ -551,7 +630,7 @@
   justify-content: center;
   align-items: center;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 100%;
     height: 100px;
     border: 0;
@@ -570,14 +649,19 @@
 .uppy-DashboardItem-sourceIcon {
   display: inline-block;
   vertical-align: middle;
-  width: 10px;
-  height: 10px;
-  color: rgba($color-gray, 0.6);
+  width: 11px;
+  height: 11px;
+  color: rgba($color-gray, 0.85);
+}
 
-  .uppy-Dashboard--wide & {
-    width: 10px;
-    height: 10px;
-  }
+.uppy-DashboardItem-sourceIcon svg,
+.uppy-DashboardItem-sourceIcon svg * {
+  max-width: 100%;
+  max-height: 100%;
+  display: inline-block;
+  vertical-align: text-top;
+  overflow: hidden;
+  fill: currentColor;
 }
 
 .uppy-DashboardItem-previewInnerWrap {
@@ -592,12 +676,8 @@
   box-shadow: 0 0 2px 0 rgba($color-gray, 0.7);
   border-radius: 3px;
 
-  .uppy-Dashboard--wide & {
-    // box-shadow: 0 0 2px 0 rgba(175, 175, 175, 0.7);
-    box-shadow: 0 1px 3px rgba(0,0,0,.2);
-    border-radius: 3px;
-    // border-top-left-radius: 6px;
-    // border-top-right-radius: 6px;
+  .uppy-size--md & {
+    box-shadow: 0 1px 3px rgba($color-black,.2);
   }
 }
 
@@ -642,7 +722,7 @@
   left: 50%;
   transform: translate(-50%, -50%);
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 25px;
     height: 25px;
   }
@@ -662,19 +742,15 @@
 }
 
 .uppy-DashboardItem-info {
-  // padding: 10px 19px 0 25px;
   padding-left: 15px;
   position: relative;
   max-width: 65%;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 100%;
     max-width: 100%;
     flex: 1;
-    padding: 10px 19px 0 3px;
-    // border-bottom-left-radius: 6px;
-    // border-bottom-right-radius: 6px;
-    // border: 1px solid rgba($color-gray, 0.2);
+    padding: 8px 3px 0 3px;
     border-top: 0;
   }
 }
@@ -686,14 +762,15 @@
   margin: 0;
   padding: 0;
   max-height: 28px;
-  margin-bottom: 3px;
+  margin-bottom: 5px;
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     word-break: break-all;
     white-space: normal;
+    overflow: initial;
   }
 }
 
@@ -704,8 +781,9 @@
 
 .uppy-DashboardItem-status {
   font-size: 11px;
+  line-height: 11px;
   font-weight: normal;
-  color: $color-gray;
+  color: darken($color-gray, 15%);
   margin-bottom: 4px;
 }
 
@@ -713,54 +791,45 @@
   display: inline-block;
   vertical-align: bottom;
   text-transform: uppercase;
-  margin-right: 3px;
 }
 
 .uppy-DashboardItem-edit,
 .uppy-DashboardItem-copyLink {
   @include reset-button;
-  font-size: 12px;
-  text-align: left;
+  display: inline-block;
+  vertical-align: bottom;
   cursor: pointer;
-  position: absolute;
-  top: 0;
-  right: -20px;
-
-  .uppy-Dashboard--wide & {
-    top: 9px;
-    right: 3px;
-  }
 }
 
-.uppy-DashboardItem-edit .UppyIcon {
+.uppy-DashboardItem-copyLink {
   width: 11px;
   height: 11px;
-  color: $color-asphalt-gray;
-
-  .uppy-Dashboard--wide & {
-    width: 12px;
-    height: 12px;
-  }
 }
 
-.uppy-DashboardItem-copyLink .UppyIcon {
-  width: 11px;
-  height: 11px;
-  color: $color-asphalt-gray;
+.uppy-DashboardItem-edit:not(:first-child),
+.uppy-DashboardItem-copyLink:not(:first-child),
+.uppy-DashboardItem-sourceIcon:not(:first-child) {
+  position: relative;
+  margin-left: 14px;
+  // margin-right: 7px;
 
-  .uppy-Dashboard--wide & {
-    width: 13px;
-    height: 13px;
+  &:before {
+    content: '\00B7';
+    position: absolute;
+    top: 0;
+    left: -9px;
+    color: $color-gray;
+    font-weight: 700;
   }
 }
 
 .uppy-DashboardItem-action {
   position: absolute;
   top: 23px;
-  right: 5px;
+  right: 10px;
   z-index: $zIndex-3;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     top: -8px;
     right: -8px;
   }
@@ -769,14 +838,14 @@
 .uppy-DashboardItem-remove {
   @include reset-button;
   cursor: pointer;
-  color: lighten($color-asphalt-gray, 20%);
+  color: $color-black;
   width: 16px;
   height: 16px;
+  opacity: 0.75;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 20px;
     height: 20px;
-    color: lighten($color-asphalt-gray, 8%);
   }
 }
 
@@ -820,7 +889,7 @@
   opacity: 0.9;
   transition: all .35s ease;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 55px;
     height: 55px;
   }
@@ -834,7 +903,7 @@
     width: 18px;
     height: 18px;
 
-    .uppy-Dashboard--wide & {
+    .uppy-size--md & {
       width: 28px;
       height: 28px;
     }
@@ -845,7 +914,7 @@
     height: 18px;
     opacity: 1;
 
-    .uppy-Dashboard--wide & {
+    .uppy-size--md & {
       width: 25px;
       height: 25px;
     }
@@ -863,7 +932,7 @@
   width: 100%;
   text-shadow: 0 1px 0 rgba($color-black, 0.3);
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     display: block;
   }
 }
@@ -977,13 +1046,14 @@
 
 .uppy-Dashboard-actions {
   height: 55px;
-  border-top: 1px solid rgba($color-gray, 0.2);
+  border-top: 1px solid rgba($color-gray, 0.3);
   display: flex;
   align-items: center;
   padding: 0 15px;
+  background-color: $color-almost-white;
 }
 
-  .uppy-Dashboard--wide .uppy-Dashboard-actions {
+  .uppy-size--md .uppy-Dashboard-actions {
     height: 65px;
   }
 
@@ -1001,7 +1071,7 @@
   width: 50px;
   height: 50px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 60px;
     height: 60px;
   }
@@ -1025,7 +1095,7 @@
   line-height: 16px;
   font-size: 8px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 18px;
     height: 18px;
     line-height: 18px;
@@ -1038,9 +1108,8 @@
 //
 
 .uppy-DashboardFileCard {
-  transform: translate3d(0, 0, 0);
-  transition: transform 0.2s ease-in-out;
-
+  // transform: translate3d(0, 0, 0);
+  // transition: transform 0.2s ease-in-out;
   width: 100%;
   height: 100%;
   position: absolute;
@@ -1048,24 +1117,22 @@
   left: 0;
   right: 0;
   bottom: 0;
-  z-index: $zIndex-4;
+  z-index: $zIndex-5;
   box-shadow: 0px 0px 10px 4px rgba($color-black, 0.1);
   background-color: $color-white;
+  display: flex;
+  flex-direction: column;
 }
 
-  .uppy-DashboardFileCard[aria-hidden=true] {
-    transform: translate3d(0, -105%, 0);
-  }
+  // .uppy-DashboardFileCard[aria-hidden=true] {
+  //   transform: translate3d(0, -105%, 0);
+  // }
 
 .uppy-DashboardFileCard-inner {
   display: flex;
   flex-direction: column;
   height: 100%;
-  padding-top: 40px;
-
-  .uppy-Dashboard--wide & {
-    padding-top: 50px;
-  }
+  flex: 1;
 }
 
 .uppy-DashboardFileCard-preview {
@@ -1111,7 +1178,7 @@
   font-size: 12px;
   color: $color-asphalt-gray;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     font-size: 13px;
   }
 }

+ 0 - 0
packages/@uppy/dashboard/src/copyToClipboard.js → packages/@uppy/dashboard/src/utils/copyToClipboard.js


+ 0 - 0
packages/@uppy/dashboard/src/copyToClipboard.test.js → packages/@uppy/dashboard/src/utils/copyToClipboard.test.js


+ 1 - 1
packages/@uppy/dashboard/src/getFileTypeIcon.js → packages/@uppy/dashboard/src/utils/getFileTypeIcon.js

@@ -1,4 +1,4 @@
-const { iconText, iconAudio, iconVideo, iconPDF } = require('./icons')
+const { iconText, iconAudio, iconVideo, iconPDF } = require('../components/icons')
 
 module.exports = function getIconByMime (fileType) {
   const defaultChoice = {

+ 17 - 0
packages/@uppy/dashboard/src/utils/ignoreEvent.js

@@ -0,0 +1,17 @@
+// ignore drop/paste events if they are not in input or textarea —
+// otherwise when Url plugin adds drop/paste listeners to this.el,
+// draging UI elements or pasting anything into any field triggers those events —
+// Url treats them as URLs that need to be imported
+
+function ignoreEvent (ev) {
+  const tagName = ev.target.tagName
+  if (tagName === 'INPUT' ||
+      tagName === 'TEXTAREA') {
+    ev.stopPropagation()
+    return
+  }
+  ev.preventDefault()
+  ev.stopPropagation()
+}
+
+module.exports = ignoreEvent

+ 0 - 0
packages/@uppy/dashboard/src/truncateString.js → packages/@uppy/dashboard/src/utils/truncateString.js


+ 0 - 0
packages/@uppy/dashboard/src/truncateString.test.js → packages/@uppy/dashboard/src/utils/truncateString.test.js


+ 1 - 1
packages/@uppy/drag-drop/package.json

@@ -27,7 +27,7 @@
   },
   "dependencies": {
     "@uppy/utils": "0.26.0",
-    "drag-drop": "^2.14.0",
+    "drag-drop": "2.13.3",
     "preact": "^8.2.9"
   },
   "devDependencies": {

+ 0 - 14
packages/@uppy/dropbox/src/icons.js

@@ -1,14 +0,0 @@
-const { h } = require('preact')
-
-module.exports = {
-  folder: () => (
-    <svg aria-hidden="true" class="UppyIcon" style={{ width: 16, marginRight: 3 }} viewBox="0 0 276.157 276.157">
-      <path d="M273.08 101.378c-3.3-4.65-8.86-7.32-15.254-7.32h-24.34V67.59c0-10.2-8.3-18.5-18.5-18.5h-85.322c-3.63 0-9.295-2.875-11.436-5.805l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.73c-9.292 0-18.638 6.608-21.737 15.372l-2.033 5.752c-.958 2.71-4.72 5.37-7.596 5.37H18.5C8.3 49.09 0 57.39 0 67.59v167.07c0 .886.16 1.73.443 2.52.152 3.306 1.18 6.424 3.053 9.064 3.3 4.652 8.86 7.32 15.255 7.32h188.487c11.395 0 23.27-8.425 27.035-19.18l40.677-116.188c2.11-6.035 1.43-12.164-1.87-16.816zM18.5 64.088h8.864c9.295 0 18.64-6.607 21.738-15.37l2.032-5.75c.96-2.712 4.722-5.373 7.597-5.373h29.565c3.63 0 9.295 2.876 11.437 5.806l6.386 8.735c4.982 6.815 15.104 11.954 23.546 11.954h85.322c1.898 0 3.5 1.602 3.5 3.5v26.47H69.34c-11.395 0-23.27 8.423-27.035 19.178L15 191.23V67.59c0-1.898 1.603-3.5 3.5-3.5zm242.29 49.15l-40.676 116.188c-1.674 4.78-7.812 9.135-12.877 9.135H18.75c-1.447 0-2.576-.372-3.02-.997-.442-.625-.422-1.814.057-3.18l40.677-116.19c1.674-4.78 7.812-9.134 12.877-9.134h188.487c1.448 0 2.577.372 3.02.997.443.625.423 1.814-.056 3.18z" />
-    </svg>
-  ),
-  file: () => (
-    <svg aria-hidden="true" class="UppyIcon" width={11} height={14.5} viewBox="0 0 44 58">
-      <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
-    </svg>
-  )
-}

+ 3 - 4
packages/@uppy/dropbox/src/index.js

@@ -1,7 +1,6 @@
 const { Plugin } = require('@uppy/core')
 const { Provider } = require('@uppy/companion-client')
 const ProviderViews = require('@uppy/provider-views')
-const icons = require('./icons')
 const { h } = require('preact')
 
 module.exports = class Dropbox extends Plugin {
@@ -9,9 +8,9 @@ module.exports = class Dropbox extends Plugin {
     super(uppy, opts)
     this.id = this.opts.id || 'Dropbox'
     Provider.initPlugin(this, opts)
-    this.title = 'Dropbox'
+    this.title = this.opts.title || 'Dropbox'
     this.icon = () => (
-      <svg class="UppyIcon" width="128" height="118" viewBox="0 0 128 118">
+      <svg aria-hidden="true" fill="#0060ff" width="128" height="118" viewBox="0 0 128 118">
         <path d="M38.145.777L1.108 24.96l25.608 20.507 37.344-23.06z" />
         <path d="M1.108 65.975l37.037 24.183L64.06 68.525l-37.343-23.06zM64.06 68.525l25.917 21.633 37.036-24.183-25.61-20.51z" />
         <path d="M127.014 24.96L89.977.776 64.06 22.407l37.345 23.06zM64.136 73.18l-25.99 21.567-11.122-7.262v8.142l37.112 22.256 37.114-22.256v-8.142l-11.12 7.262z" />
@@ -72,7 +71,7 @@ module.exports = class Dropbox extends Plugin {
   }
 
   getItemIcon (item) {
-    return icons[item['.tag']]()
+    return item['.tag']
   }
 
   getItemSubList (item) {

+ 11 - 5
packages/@uppy/google-drive/src/index.js

@@ -7,12 +7,18 @@ module.exports = class GoogleDrive extends Plugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.id = this.opts.id || 'GoogleDrive'
+    this.title = this.opts.title || 'Google Drive'
     Provider.initPlugin(this, opts)
-    this.title = 'Google Drive'
-    this.icon = () =>
-      <svg aria-hidden="true" class="UppyIcon UppyModalTab-icon" width="28" height="28" viewBox="0 0 16 16">
-        <path d="M2.955 14.93l2.667-4.62H16l-2.667 4.62H2.955zm2.378-4.62l-2.666 4.62L0 10.31l5.19-8.99 2.666 4.62-2.523 4.37zm10.523-.25h-5.333l-5.19-8.99h5.334l5.19 8.99z" />
+    this.title = this.opts.title || 'Google Drive'
+    this.icon = () => (
+      <svg aria-hidden="true" width="18px" height="16px" viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+        <g fill-rule="evenodd">
+          <polygon fill="#3089FC" points="6.32475 10.2 18 10.2 14.999625 15.3 3.324375 15.3" />
+          <polygon fill="#00A85D" points="3.000375 15.3 0 10.2 5.83875 0.275974026 8.838 5.37597403 5.999625 10.2" />
+          <polygon fill="#FFD024" points="11.838375 9.92402597 5.999625 0 12.000375 0 17.839125 9.92402597" />
+        </g>
       </svg>
+    )
 
     this[this.id] = new Provider(uppy, {
       serverUrl: this.opts.serverUrl,
@@ -77,7 +83,7 @@ module.exports = class GoogleDrive extends Plugin {
   }
 
   getItemIcon (item) {
-    return <img src={item.iconLink} />
+    return item.iconLink
   }
 
   getItemSubList (item) {

+ 6 - 7
packages/@uppy/informer/src/index.js

@@ -44,20 +44,19 @@ module.exports = class Informer extends Plugin {
   }
 
   render (state) {
-    const { isHidden, type, message, details } = state.info
-    const style = {
-      backgroundColor: this.opts.typeColors[type].bg,
-      color: this.opts.typeColors[type].text
-    }
+    const { isHidden, message, details } = state.info
+    // const style = {
+    //   backgroundColor: this.opts.typeColors[type].bg,
+    //   color: this.opts.typeColors[type].text
+    // }
 
     return (
       <div class="uppy uppy-Informer"
-        style={style}
         aria-hidden={isHidden}>
         <p role="alert">
           {message}
           {' '}
-          {details && <span style={{ color: this.opts.typeColors[type].bg }}
+          {details && <span
             aria-label={details}
             data-microtip-position="top"
             data-microtip-size="large"

+ 36 - 21
packages/@uppy/informer/src/style.scss

@@ -3,44 +3,58 @@
 
 .uppy-Informer {
   position: absolute;
-  bottom: 0;
+  bottom: 60px;
   left: 0;
   right: 0;
   text-align: center;
-  font-size: 12px;
-  font-weight: 500;
-  padding: 0 15px;
-  height: 35px;
-  line-height: 35px;
-  background-color: $color-black; /* no !important */
-  color: $color-white;
+  // padding: 0 15px;
+  // height: 25px;
+  // line-height: 25px;
+
   opacity: 1;
   transform: none;
-  transition: all 300ms ease-in;
-  z-index: $zIndex-4;
+  transition: all 250ms ease-in;
+  z-index: $zIndex-5;
+  // border-radius: 18px;
+  // max-width: 100%;
+  // margin: auto;
+  
 
-  .uppy-Dashboard--wide & {
-    height: 45px;
-    line-height: 45px;
-    font-size: 13px;
-  }
+  // .uppy-size--md & {
+  //   height: 35px;
+  //   line-height: 35px;
+  //   font-size: 12px;
+  //   // max-width: 500px;
+  //   padding: 0 15px;
+  // }
 }
 
   .uppy-Informer[aria-hidden=true] {
     opacity: 0;
-    transform: translateY(200%);
+    transform: translateY(350%);
     transition: all 300ms ease-in;
   }
 
 .uppy-Informer p {
+  display: inline-block;
   margin: 0;
   padding: 0;
-  height: 35px;
-  line-height: 35px;
+  // height: 25px;
+  // line-height: 25px;
+  font-size: 12px;
+  line-height: 1.4;
+  font-weight: 400;
+  padding: 6px 15px;
+  background-color: rgba($color-asphalt-gray, 0.8); /* no !important */
+  color: $color-white;
+  border-radius: 18px;
+  max-width: 90%;
 
-  .uppy-Dashboard--wide & {
-    height: 45px;
-    line-height: 45px;
+  .uppy-size--md & {
+    font-size: 14px;
+    line-height: 1.3;
+    max-width: 500px;
+    padding: 10px 20px;
   }
 }
 
@@ -50,6 +64,7 @@
   height: 13px;
   display: inline-block;
   vertical-align: middle;
+  color: $color-asphalt-gray;
   background-color: $color-white;
   border-radius: 50%;
   position: relative;

+ 5 - 7
packages/@uppy/instagram/src/index.js

@@ -8,9 +8,9 @@ module.exports = class Instagram extends Plugin {
     super(uppy, opts)
     this.id = this.opts.id || 'Instagram'
     Provider.initPlugin(this, opts)
-    this.title = 'Instagram'
+    this.title = this.opts.title || 'Instagram'
     this.icon = () => (
-      <svg aria-hidden="true" class="UppyIcon" width="28" height="28" viewBox="0 0 512 512">
+      <svg aria-hidden="true" fill="#DE3573" width="28" height="28" viewBox="0 0 512 512">
         <path d="M256,49.471c67.266,0,75.233.257,101.8,1.469,24.562,1.121,37.9,5.224,46.778,8.674a78.052,78.052,0,0,1,28.966,18.845,78.052,78.052,0,0,1,18.845,28.966c3.45,8.877,7.554,22.216,8.674,46.778,1.212,26.565,1.469,34.532,1.469,101.8s-0.257,75.233-1.469,101.8c-1.121,24.562-5.225,37.9-8.674,46.778a83.427,83.427,0,0,1-47.811,47.811c-8.877,3.45-22.216,7.554-46.778,8.674-26.56,1.212-34.527,1.469-101.8,1.469s-75.237-.257-101.8-1.469c-24.562-1.121-37.9-5.225-46.778-8.674a78.051,78.051,0,0,1-28.966-18.845,78.053,78.053,0,0,1-18.845-28.966c-3.45-8.877-7.554-22.216-8.674-46.778-1.212-26.564-1.469-34.532-1.469-101.8s0.257-75.233,1.469-101.8c1.121-24.562,5.224-37.9,8.674-46.778A78.052,78.052,0,0,1,78.458,78.458a78.053,78.053,0,0,1,28.966-18.845c8.877-3.45,22.216-7.554,46.778-8.674,26.565-1.212,34.532-1.469,101.8-1.469m0-45.391c-68.418,0-77,.29-103.866,1.516-26.815,1.224-45.127,5.482-61.151,11.71a123.488,123.488,0,0,0-44.62,29.057A123.488,123.488,0,0,0,17.3,90.982C11.077,107.007,6.819,125.319,5.6,152.134,4.369,179,4.079,187.582,4.079,256S4.369,333,5.6,359.866c1.224,26.815,5.482,45.127,11.71,61.151a123.489,123.489,0,0,0,29.057,44.62,123.486,123.486,0,0,0,44.62,29.057c16.025,6.228,34.337,10.486,61.151,11.71,26.87,1.226,35.449,1.516,103.866,1.516s77-.29,103.866-1.516c26.815-1.224,45.127-5.482,61.151-11.71a128.817,128.817,0,0,0,73.677-73.677c6.228-16.025,10.486-34.337,11.71-61.151,1.226-26.87,1.516-35.449,1.516-103.866s-0.29-77-1.516-103.866c-1.224-26.815-5.482-45.127-11.71-61.151a123.486,123.486,0,0,0-29.057-44.62A123.487,123.487,0,0,0,421.018,17.3C404.993,11.077,386.681,6.819,359.866,5.6,333,4.369,324.418,4.079,256,4.079h0Z" />
         <path d="M256,126.635A129.365,129.365,0,1,0,385.365,256,129.365,129.365,0,0,0,256,126.635Zm0,213.338A83.973,83.973,0,1,1,339.974,256,83.974,83.974,0,0,1,256,339.973Z" />
         <circle cx="390.476" cy="121.524" r="30.23" />
@@ -78,11 +78,9 @@ module.exports = class Instagram extends Plugin {
 
   getItemIcon (item) {
     if (!item.images) {
-      return <svg viewBox="0 0 58 58" opacity="0.6">
-        <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z" /><path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z" />
-      </svg>
+      return 'video'
     }
-    return <img src={item.images.low_resolution.url} />
+    return item.images.low_resolution.url
   }
 
   getItemSubList (item) {
@@ -114,7 +112,7 @@ module.exports = class Instagram extends Plugin {
         minute: 'numeric'
       })
       // adding both date and carousel_id, so the name is unique
-      return `Instagram ${date} ${item.carousel_id || ''}.${ext}`
+      return `Instagram ${date}${item.carousel_id ? ' ' + item.carousel_id : ''}.${ext}`
     }
     return ''
   }

+ 4 - 8
packages/@uppy/provider-views/src/AuthView.js

@@ -34,14 +34,10 @@ class AuthView extends Component {
   }
 
   render () {
-    return (
-      <div style={{ height: '100%' }}>
-        {this.props.checkAuthInProgress
-          ? <LoaderView />
-          : <AuthBlock {...this.props} />
-        }
-      </div>
-    )
+    if (this.props.checkAuthInProgress) {
+      return <LoaderView />
+    }
+    return <AuthBlock {...this.props} />
   }
 }
 

+ 3 - 3
packages/@uppy/provider-views/src/Breadcrumbs.js

@@ -2,13 +2,13 @@ const { h } = require('preact')
 
 const Breadcrumb = (props) => {
   return (
-    <li><button type="button" onclick={props.getFolder}>{props.title}</button></li>
+    <button type="button" onclick={props.getFolder}>{props.title}</button>
   )
 }
 
 module.exports = (props) => {
   return (
-    <ul class="uppy-Provider-breadcrumbs">
+    <div class="uppy-Provider-breadcrumbs">
       {
         props.directories.map((directory, i) => {
           return Breadcrumb({
@@ -17,6 +17,6 @@ module.exports = (props) => {
           })
         })
       }
-    </ul>
+    </div>
   )
 }

+ 8 - 6
packages/@uppy/provider-views/src/Browser.js

@@ -20,12 +20,14 @@ const Browser = (props) => {
     <div class={classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${props.viewType}`)}>
       <div class="uppy-ProviderBrowser-header">
         <div class={classNames('uppy-ProviderBrowser-headerBar', !props.showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple')}>
-          <div class="uppy-Provider-breadcrumbsIcon">{props.pluginIcon && props.pluginIcon()}</div>
-          {props.showBreadcrumbs && Breadcrumbs({
-            getFolder: props.getFolder,
-            directories: props.directories,
-            title: props.title
-          })}
+          <div class="uppy-Provider-breadcrumbsWrap">
+            <div class="uppy-Provider-breadcrumbsIcon">{props.pluginIcon && props.pluginIcon()}</div>
+            {props.showBreadcrumbs && Breadcrumbs({
+              getFolder: props.getFolder,
+              directories: props.directories,
+              title: props.title
+            })}
+          </div>
           <span class="uppy-ProviderBrowser-user">{props.username}</span>
           <button type="button" onclick={props.logout} class="uppy-ProviderBrowser-userLogout">
             {props.i18n('logOut')}

+ 26 - 2
packages/@uppy/provider-views/src/Item.js

@@ -1,5 +1,26 @@
 const { h } = require('preact')
 
+function mapStringToIcon (string) {
+  if (string === null) return
+
+  switch (string) {
+    case 'file':
+      return <svg aria-hidden="true" class="UppyIcon" width={11} height={14.5} viewBox="0 0 44 58">
+        <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
+      </svg>
+    case 'folder':
+      return <svg aria-hidden="true" class="UppyIcon" style={{ width: 16, marginRight: 3 }} viewBox="0 0 276.157 276.157">
+        <path d="M273.08 101.378c-3.3-4.65-8.86-7.32-15.254-7.32h-24.34V67.59c0-10.2-8.3-18.5-18.5-18.5h-85.322c-3.63 0-9.295-2.875-11.436-5.805l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.73c-9.292 0-18.638 6.608-21.737 15.372l-2.033 5.752c-.958 2.71-4.72 5.37-7.596 5.37H18.5C8.3 49.09 0 57.39 0 67.59v167.07c0 .886.16 1.73.443 2.52.152 3.306 1.18 6.424 3.053 9.064 3.3 4.652 8.86 7.32 15.255 7.32h188.487c11.395 0 23.27-8.425 27.035-19.18l40.677-116.188c2.11-6.035 1.43-12.164-1.87-16.816zM18.5 64.088h8.864c9.295 0 18.64-6.607 21.738-15.37l2.032-5.75c.96-2.712 4.722-5.373 7.597-5.373h29.565c3.63 0 9.295 2.876 11.437 5.806l6.386 8.735c4.982 6.815 15.104 11.954 23.546 11.954h85.322c1.898 0 3.5 1.602 3.5 3.5v26.47H69.34c-11.395 0-23.27 8.423-27.035 19.178L15 191.23V67.59c0-1.898 1.603-3.5 3.5-3.5zm242.29 49.15l-40.676 116.188c-1.674 4.78-7.812 9.135-12.877 9.135H18.75c-1.447 0-2.576-.372-3.02-.997-.442-.625-.422-1.814.057-3.18l40.677-116.19c1.674-4.78 7.812-9.134 12.877-9.134h188.487c1.448 0 2.577.372 3.02.997.443.625.423 1.814-.056 3.18z" />
+      </svg>
+    case 'video':
+      return <svg aria-hidden="true" viewBox="0 0 58 58">
+        <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z" /><path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z" />
+      </svg>
+    default:
+      return <img src={string} />
+  }
+}
+
 module.exports = (props) => {
   const stop = (ev) => {
     if (ev.keyCode === 13) {
@@ -17,8 +38,10 @@ module.exports = (props) => {
     props.handleClick(ev)
   }
 
+  const itemIcon = props.getItemIcon()
+
   return (
-    <li class={'uppy-ProviderBrowserItem' + (props.isChecked ? ' uppy-ProviderBrowserItem--selected' : '')}>
+    <li class={'uppy-ProviderBrowserItem' + (props.isChecked ? ' uppy-ProviderBrowserItem--selected' : '') + (itemIcon === 'video' ? ' uppy-ProviderBrowserItem--noPreview' : '')}>
       <div class="uppy-ProviderBrowserItem-checkbox">
         <input type="checkbox"
           role="option"
@@ -41,7 +64,8 @@ module.exports = (props) => {
         aria-label={`Select ${props.title}`}
         tabindex={0}
         onclick={handleItemClick}>
-        {props.getItemIcon()} {props.showTitles && props.title}
+        {mapStringToIcon(props.getItemIcon())}
+        {props.showTitles && props.title}
       </button>
     </li>
   )

+ 144 - 83
packages/@uppy/provider-views/src/style.scss

@@ -1,5 +1,12 @@
 @import '@uppy/core/src/style.scss';
 
+.uppy-DashboardContent-panelBody {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+}
+
 .uppy-Provider-auth,
 .uppy-Provider-error,
 .uppy-Provider-loading,
@@ -8,51 +15,62 @@
   align-items: center;
   justify-content: center;
   flex-flow: column wrap;
-  height: 100%;
+  flex: 1;
 }
 
-.uppy-Provider-authIcon .UppyIcon {
+.uppy-Provider-authIcon svg {
   width: 100px;
   height: 75px;
-  color: rgba($color-asphalt-gray, 0.3);
   margin-bottom: 15px;
 }
 
 .uppy-Provider-authTitle {
-  font-size: 20px;
+  font-size: 17px;
   line-height: 1.4;
   font-weight: 400;
   margin-bottom: 30px;
   padding: 0 15px;
   max-width: 500px;
   text-align: center;
+
+  .uppy-size--md & {
+    font-size: 20px;
+  }
 }
 
-.uppy-Provider-breadcrumbs {
+.uppy-Provider-breadcrumbsWrap {
   flex: 1;
+}
+
+.uppy-Provider-breadcrumbs {
+  display: inline-block;
   color: darken($color-gray, 25%);
   font-size: 12px;
-  list-style-type: none;
-  padding: 0;
-  margin: 0;
+  line-height: 1;
+  margin-bottom: 10px;
+
+  .uppy-size--md & {
+    margin-bottom: 0;
+  }
 }
 
 .uppy-Provider-breadcrumbsIcon {
   display: inline;
   color: darken($color-gray, 25%);
   vertical-align: middle;
-  // position: relative;
-  // top: 1px;
   margin-right: 8px;
+  line-height: 1;
 }
 
-  .uppy-Provider-breadcrumbsIcon .UppyIcon {
+  .uppy-Provider-breadcrumbsIcon svg {
     width: 13px;
     height: 13px;
+    fill: darken($color-gray, 25%);
   }
 
 .uppy-Provider-breadcrumbs button {
   @include reset-button;
+  display: inline-block;
   cursor: pointer;
   font-size: 14px;
 }
@@ -61,16 +79,11 @@
   text-decoration: underline;
 }
 
-.uppy-Provider-breadcrumbs button:focus {
-  outline: 1px dotted rgb(145, 145, 145);
-}
-
-.uppy-Provider-breadcrumbs li {
-  display: inline-block;
-  margin: 0;
-}
+// .uppy-Provider-breadcrumbs button:focus {
+//   outline: 1px dotted rgb(145, 145, 145);
+// }
 
-.uppy-Provider-breadcrumbs li ~ li:before {
+.uppy-Provider-breadcrumbs button ~ button:before {
   content: '/';
   padding: 0 7px;
 }
@@ -78,6 +91,7 @@
 .uppy-ProviderBrowser {
   display: flex;
   flex-direction: column;
+  flex: 1;
   font-size: 13px;
   font-weight: 400;
   height: 100%;
@@ -85,6 +99,7 @@
 
 .uppy-ProviderBrowser-user {
   margin: 0 8px 0 0;
+  line-height: 1;
 }
 
   .uppy-ProviderBrowser-user:after {
@@ -95,32 +110,44 @@
 
 .uppy-ProviderBrowser-header {
   z-index: $zIndex-2;
-  border-bottom: 1px solid lighten($color-asphalt-gray, 60%);
+  border-bottom: 1px solid rgba($color-gray, 0.3);
   position: relative;
 }
 
 .uppy-ProviderBrowser-headerBar {
-  height: 40px;
-  line-height: 40px;
-  display: flex;
-  align-items: center;
-  padding: 0 16px;
+  padding: 12px 15px;
   background-color: lighten($color-gray, 40%);
   z-index: $zIndex-2;
   color: darken($color-gray, 20%);
-}
+  line-height: 1;
 
-.uppy-ProviderBrowser-headerBar--simple {
-  text-align: center;
-  display: block;
+  .uppy-size--md & {
+    display: flex;
+    align-items: center;
+    height: 40px;
+    line-height: 40px;
+    padding: 0 15px;
+  }
 }
 
+  .uppy-ProviderBrowser-headerBar--simple {
+    text-align: center;
+    display: block;
+    justify-content: center;
+  }
+
+  .uppy-ProviderBrowser-headerBar--simple .uppy-Provider-breadcrumbsWrap {
+    flex: none;
+    display: inline-block;
+    vertical-align: middle;
+  }
+
 .uppy-ProviderBrowser-search {
   width: 100%;
   background-color: $color-white;
   position: relative;
   height: 30px;
-  margin-top: 15px;
+  margin-top: 5px;
   margin-bottom: 5px;
 }
 
@@ -169,7 +196,9 @@
   @include reset-button();
   cursor: pointer;
 
-  &:hover { text-decoration: underline; }
+  &:hover { 
+    text-decoration: underline; 
+  }
 }
 
 .uppy-ProviderBrowser-body {
@@ -187,6 +216,7 @@
   border-spacing: 0;
   overflow-x: hidden;
   overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   position: absolute;
   top: 0;
   bottom: 0;
@@ -208,37 +238,38 @@
 // ***
 
 .uppy-ProviderBrowser-viewType--list {
-
   background-color: $color-white;
 
-  .uppy-ProviderBrowser-list {
-    // padding-top: 6px;
-  }
-
   .uppy-ProviderBrowserItem {
+    display: flex;
     padding: 10px 15px;
   }
 
-  .uppy-ProviderBrowserItem-inner {
-    max-width: 80%;
-    word-wrap: break-word;
-    text-align: left;
-    line-height: 1.4;
+  .uppy-ProviderBrowserItem-checkbox {
+    vertical-align: middle;
   }
 
-  .uppy-ProviderBrowserItem-inner img {
-    vertical-align: text-top;
-    margin-right: 3px;
-  }
+    .uppy-ProviderBrowserItem-checkbox label:before {
+      border-color: rgba($color-asphalt-gray, 0.4);
+    }
 
-  .uppy-ProviderBrowserItem-checkbox label:before {
-    border-color: rgba($color-asphalt-gray, 0.4);
-  }
+    .uppy-ProviderBrowserItem-checkbox input:checked + label:before {
+      border-color: $color-cornflower-blue;
+    }
 
-  .uppy-ProviderBrowserItem-checkbox input:checked + label:before {
-    border-color: $color-cornflower-blue;
+  .uppy-ProviderBrowserItem-inner {
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    text-align: left;
+    line-height: 1.4;
   }
 
+  .uppy-ProviderBrowserItem-inner img,
+  .uppy-ProviderBrowserItem-inner svg {
+    vertical-align: top;
+    margin-right: 8px;
+  }
 }
 
 // ***
@@ -265,12 +296,12 @@
     display: inline-block;
     width: 50%;
     position: relative;
-    padding: 8px;
   }
 
-    .uppy-Dashboard--wide .uppy-ProviderBrowserItem {
-      width: 33.3333%;
-      padding: 12px;
+    .uppy-ProviderBrowserItem:before {
+      content: '';
+      padding-top: 100%;
+      display: block;
     }
 
     // .uppy-ProviderBrowserItem--selected {
@@ -278,52 +309,83 @@
     //   outline: none;
     // }
 
-    .uppy-ProviderBrowserItem--selected .uppy-ProviderBrowserItem-inner {
-      box-shadow: 0 0 0 3px rgba(darken($color-cornflower-blue, 10%), 0.9);
-    }
+    // .uppy-ProviderBrowserItem--selected .uppy-ProviderBrowserItem-inner {
+    //   box-shadow: 0 0 0 3px rgba(darken($color-cornflower-blue, 10%), 0.9);
+    // }
 
   .uppy-ProviderBrowserItem-inner {
-    width: 100%;
-    height: 100%;
     border-radius: 4px;
     overflow: hidden;
-    border: 2px solid transparent;
+    position: absolute;
+    top: 7px;
+    left: 7px;
+    right: 7px;
+    bottom: 7px;
+  }
+
+  .uppy-ProviderBrowserItem-inner:focus {
+    outline: none;
+    box-shadow: 0 0 0 3px rgba($color-cornflower-blue, 0.9);
   }
 
-  .uppy-ProviderBrowserItem img {
+  .uppy-ProviderBrowserItem img,
+  .uppy-ProviderBrowserItem svg {
     width: 100%;
     height: 100%;
     vertical-align: middle;
+    object-fit: cover;
+  }
+
+  .uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner {
+    background-color: rgba($color-gray, 0.3);
+  }
+
+  .uppy-ProviderBrowserItem--noPreview svg {
+    fill: rgba($color-black, 0.7);
+    width: 30%;
+    height: 30%;
   }
 
   .uppy-ProviderBrowserItem-checkbox {
-    display: none;
+    position: absolute;
+    top: 13px;
+    right: 22px;
+    margin-right: 0;
+    opacity: 0.95;
+    z-index: $zIndex-3;
   }
 
-  // .uppy-ProviderBrowserItem-checkbox {
-  //   position: absolute;
-  //   top: 8px;
-  //   right: 10px;
-  //   margin-right: 0;
-  // }
+  .uppy-ProviderBrowserItem-checkbox label:before {
+    background-color: $color-cornflower-blue;
+    border-radius: 50%;
+    width: 26px;
+    height: 26px;
+  }
 
-  // .uppy-ProviderBrowserItem-checkbox label:before {
-  //   background-color: $color-cornflower-blue;
-  // }
+  .uppy-ProviderBrowserItem-checkbox label:after {
+    width: 12px;
+    height: 7px;
+    left: 7px;
+    top: 10px;
+  }
 
-  // // Hide checkbox when unchecked in grid view
-  // .uppy-ProviderBrowserItem-checkbox input + label {
-  //   opacity: 0;
-  // }
+  // Hide checkbox when unchecked in grid view
+  .uppy-ProviderBrowserItem-checkbox input + label {
+    opacity: 0;
+  }
+
+  // Unhide the checkbox on the checked state
+  .uppy-ProviderBrowserItem-checkbox input:checked + label {
+    opacity: 1;
+  }
 
-  // // Unhide the checkbox on the checked state
-  // .uppy-ProviderBrowserItem-checkbox input:checked + label {
-  //   opacity: 1;
-  // }
+}
 
+.uppy-size--md .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem {
+  width: 33.3333%;
 }
 
-.uppy-Dashboard--wide .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem {
+.uppy-size--lg .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem {
   width: 25%;
 }
 
@@ -336,7 +398,7 @@
   position: relative;
   display: inline-block;
   top: -3px;
-  margin-right: 20px;
+  margin-right: 15px;
 }
 
 .uppy-ProviderBrowserItem-checkbox label {
@@ -359,7 +421,6 @@
   border: 1px solid $color-cornflower-blue;
   background-color: $color-white;
   border-radius: 2px;
-  // border-radius: 50%;
 }
 
 // Inner checkbox

+ 4 - 5
packages/@uppy/status-bar/src/StatusBar.js

@@ -177,11 +177,11 @@ const ProgressBarProcessing = (props) => {
 }
 
 const progressDetails = (props) => {
-  return <span class="uppy-StatusBar-statusSecondary">
+  return <div class="uppy-StatusBar-statusSecondary">
     { props.inProgress > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.inProgress }) + ' \u00B7 ' }
     { props.i18n('dataUploadedOfTotal', { complete: props.totalUploadedSize, total: props.totalSize }) + ' \u00B7 ' }
     { props.i18n('xTimeLeft', { time: props.totalETA }) }
-  </span>
+  </div>
 }
 
 const ThrottledProgressDetails = throttle(progressDetails, 500, { leading: true, trailing: true })
@@ -197,8 +197,7 @@ const ProgressBarUploading = (props) => {
     <div class="uppy-StatusBar-content" aria-label={title} title={title}>
       { !props.hidePauseResumeCancelButtons && <PauseResumeButtons {...props} /> }
       <div class="uppy-StatusBar-status">
-        <span class="uppy-StatusBar-statusPrimary">{title}: {props.totalProgress}%</span>
-        <br />
+        <div class="uppy-StatusBar-statusPrimary">{title}: {props.totalProgress}%</div>
         { !props.isAllPaused && <ThrottledProgressDetails {...props} /> }
       </div>
     </div>
@@ -219,7 +218,7 @@ const ProgressBarComplete = ({ totalProgress, i18n }) => {
 const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
   return (
     <div class="uppy-StatusBar-content" role="alert">
-      <strong class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</strong>
+      <span class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</span>
       { !hideRetryButton && <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span> }
       <span class="uppy-StatusBar-details"
         aria-label={error}

+ 77 - 39
packages/@uppy/status-bar/src/style.scss

@@ -9,18 +9,28 @@
   font-size: 12px;
   font-weight: 400;
   color: $color-white;
-  background-color: lighten($color-black, 10%);
-  // box-shadow: 1px 1px 4px 0 rgba($color-asphalt-gray, 0.3);
-  // border-top: 1px solid rgba($color-gray, 0.2);
+  background-color: $color-white;
   z-index: $zIndex-2;
   transition: height .2s;
 }
 
-  .uppy-Dashboard--wide .uppy-StatusBar {
+  .uppy-size--md .uppy-StatusBar {
     height: 45px;
     font-size: 14px;
   }
 
+  .uppy-StatusBar:before {
+    content: '';
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    width: 100%;
+    height: 2px;
+    background-color: rgba($color-gray, 0.25);
+  }
+
 .uppy-StatusBar[aria-hidden=true] {
   overflow-y: hidden;
   height: 0;
@@ -35,14 +45,18 @@
 }
 
 .uppy-StatusBar.is-complete .uppy-StatusBar-content {
-  width: 100%;
-  text-align: center;
-  padding-left: 0;
-  justify-content: center;
+  // width: 100%;
+  // text-align: center;
+  // padding-left: 0;
+  // justify-content: center;
+}
+
+.uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator {
+  cursor: default;
+  color: $color-green;
 }
 
 .uppy-StatusBar:not([aria-hidden=true]).is-waiting {
-  // background-color: darken($color-white, 2%);
   background-color: $color-white;
   height: 65px;
   border-top: 1px solid rgba($color-gray, 0.3);
@@ -50,7 +64,7 @@
 
 .uppy-StatusBar-progress {
   background-color: $color-cornflower-blue;
-  height: 100%;
+  height: 2px;
   position: absolute;
   z-index: $zIndex-2;
   transition: background-color, width .3s ease-out;
@@ -80,7 +94,8 @@
   padding-left: 15px;
   white-space: nowrap;
   text-overflow: ellipsis;
-  color: $color-white;
+  // color: $color-white;
+  color: $color-black;
   height: 100%;
 }
 
@@ -89,9 +104,11 @@
 }
 
 .uppy-StatusBar-status {
-  line-height: 1.35;
+  line-height: 1.5;
   font-weight: normal;
-  letter-spacing: 0.5px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
 }
 
 .uppy-StatusBar-statusPrimary {
@@ -101,18 +118,31 @@
 .uppy-StatusBar-statusSecondary {
   font-size: 11px;
   display: none;
+  color: rgba($color-asphalt-gray, 0.8);
+  max-width: 170px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+
+  .uppy-size--md & {
+    max-width: 500px;
+  }
 }
 
   .uppy-StatusBar--detailedProgress .uppy-StatusBar-statusSecondary {
-    display: inline;
+    display: inline-block;
   }
 
 .uppy-StatusBar-statusIndicator {
-  color: $color-white;
+  color: $color-asphalt-gray;
   margin-right: 15px;
   cursor: pointer;
 }
 
+  .uppy-StatusBar-statusIndicator svg {
+    vertical-align: text-bottom;
+  }
+
   .uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator  {
     width: 15px;
     margin-right: 7px;
@@ -124,7 +154,7 @@
   position: absolute;
   top: 0;
   bottom: 0;
-  right: 15px;
+  right: 10px;
   z-index: $zIndex-4;
 }
 
@@ -132,26 +162,34 @@
   width: 100%;
   position: static;
   padding: 0 15px;
+  background-color: $color-almost-white;
 }
 
 .uppy-StatusBar-actionBtn {
   font-size: 12px;
   padding: 6px;
-  border-radius: 4px;
+  // border-radius: 4px;
+  color: $color-cornflower-blue;
 }
 
-  .uppy-Dashboard--wide .uppy-StatusBar-actionBtn {
-    padding: 7px 10px;
+  .uppy-size--md .uppy-StatusBar-actionBtn {
+    padding: 3px 5px;
   }
 
   .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload {
     font-size: 14px;
     width: 100%;
     padding: 15px 10px;
+    color: $color-white;
+    background-color: $color-green;
   }
 
-    .uppy-Dashboard--wide .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload {
-      padding: 13px 28px;
+    .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover {
+      background-color: darken($color-green, 10%);
+    }
+
+    .uppy-size--md .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload {
+      padding: 16px 22px;
       width: auto;
     }
 
@@ -161,23 +199,23 @@
 
   .uppy-StatusBar:not(.is-waiting) .uppy-StatusBar-actionBtn--upload {
     background-color: transparent;
-    border: 1px solid $color-white;
-    color: $color-white;
+    // border: 1px solid $color-white;
+    color: $color-cornflower-blue;
   }
 
-  .uppy-StatusBar-actionBtn--retry {
-    background-color: $color-white;
-    color: $color-red;
-    border: 1px solid transparent;
-  }
+  // .uppy-StatusBar-actionBtn--retry {
+  //   background-color: $color-white;
+  //   color: $color-red;
+  //   border: 1px solid transparent;
+  // }
 
-  .uppy-StatusBar-actionBtn--cancel {
-    // background-color: lighten($color-asphalt-gray, 8%);
-    // border: 1px solid lighten($color-black, 10%);
-    background-color: transparent;
-    border: 1px solid $color-white;
-    color: $color-white;
-  }
+  // .uppy-StatusBar-actionBtn--cancel {
+  //   // background-color: lighten($color-asphalt-gray, 8%);
+  //   // border: 1px solid lighten($color-black, 10%);
+  //   background-color: transparent;
+  //   border: 1px solid $color-white;
+  //   color: $color-white;
+  // }
 
 .uppy-StatusBar-details {
   line-height: 12px;
@@ -185,12 +223,12 @@
   height: 13px;
   display: inline-block;
   vertical-align: middle;
-  color: $color-red;
-  background-color: $color-white;
+  color: $color-white;
+  background-color: rgba($color-black, 0.2);
   border-radius: 50%;
   position: relative;
-  top: -1px;
-  left: 6px;
+  top: 0;
+  left: 2px;
   font-size: 10px;
   text-align: center;
   cursor: help;

+ 21 - 24
packages/@uppy/url/src/index.js

@@ -13,14 +13,10 @@ module.exports = class Url extends Plugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.id = this.opts.id || 'Url'
-    this.title = 'Link'
+    this.title = this.opts.title || 'Link'
     this.type = 'acquirer'
-    this.icon = () => <svg aria-hidden="true" class="UppyIcon UppyModalTab-icon" width="64" height="64" viewBox="0 0 64 64">
-      <circle cx="32" cy="32" r="31" />
-      <g fill-rule="nonzero" fill="#FFF">
-        <path d="M25.774 47.357a4.077 4.077 0 0 1-5.76 0L16.9 44.24a4.076 4.076 0 0 1 0-5.758l5.12-5.12-1.817-1.818-5.12 5.122a6.651 6.651 0 0 0 0 9.392l3.113 3.116a6.626 6.626 0 0 0 4.699 1.943c1.7 0 3.401-.649 4.697-1.943l10.241-10.243a6.591 6.591 0 0 0 1.947-4.696 6.599 6.599 0 0 0-1.947-4.696l-3.116-3.114-1.817 1.817 3.116 3.114a4.045 4.045 0 0 1 1.194 2.88 4.045 4.045 0 0 1-1.194 2.878L25.774 47.357z" />
-        <path d="M46.216 14.926a6.597 6.597 0 0 0-4.696-1.946h-.001a6.599 6.599 0 0 0-4.696 1.945L26.582 25.167a6.595 6.595 0 0 0-1.947 4.697 6.599 6.599 0 0 0 1.946 4.698l3.114 3.114 1.818-1.816-3.114-3.114a4.05 4.05 0 0 1-1.194-2.882c0-1.086.424-2.108 1.194-2.878L38.64 16.744a4.042 4.042 0 0 1 2.88-1.194c1.089 0 2.11.425 2.88 1.194l3.114 3.114a4.076 4.076 0 0 1 0 5.758l-5.12 5.12 1.818 1.817 5.12-5.122a6.649 6.649 0 0 0 0-9.393l-3.113-3.114-.003.002z" />
-      </g>
+    this.icon = () => <svg aria-hidden="true" width="23" height="23" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
+      <path d="M20.485 11.236l-2.748 2.737c-.184.182-.367.365-.642.547-1.007.73-2.107 1.095-3.298 1.095-1.65 0-3.298-.73-4.398-2.19-.275-.365-.183-1.003.183-1.277.367-.273 1.008-.182 1.283.183 1.191 1.642 3.482 1.915 5.13.73a.714.714 0 0 0 .367-.365l2.75-2.737c1.373-1.46 1.373-3.74-.093-5.108a3.72 3.72 0 0 0-5.13 0L12.33 6.4a.888.888 0 0 1-1.283 0 .88.88 0 0 1 0-1.277l1.558-1.55a5.38 5.38 0 0 1 7.605 0c2.29 2.006 2.382 5.564.274 7.662zm-8.979 6.294L9.95 19.081a3.72 3.72 0 0 1-5.13 0c-1.467-1.368-1.467-3.74-.093-5.108l2.75-2.737.366-.365c.824-.547 1.74-.82 2.748-.73 1.008.183 1.833.639 2.382 1.46.275.365.917.456 1.283.182.367-.273.458-.912.183-1.277-.916-1.186-2.199-1.915-3.573-2.098-1.374-.273-2.84.091-4.031 1.004l-.55.547-2.749 2.737c-2.107 2.189-2.015 5.655.092 7.753C4.727 21.453 6.101 22 7.475 22c1.374 0 2.749-.547 3.848-1.55l1.558-1.551a.88.88 0 0 0 0-1.278c-.367-.364-1.008-.456-1.375-.09z" fill="#FF814F" fill-rule="nonzero" />
     </svg>
 
     // Set default options and locale
@@ -183,24 +179,25 @@ module.exports = class Url extends Plugin {
   }
 
   handlePaste (e) {
-    if (e.clipboardData.items) {
-      const items = toArray(e.clipboardData.items)
-
-      // When a file is pasted, it appears as two items: file name string, then
-      // the file itself; Url then treats file name string as URL, which is wrong.
-      // This makes sure Url ignores paste event if it contains an actual file
-      const hasFiles = items.filter(item => item.kind === 'file').length > 0
-      if (hasFiles) return
-
-      items.forEach((item) => {
-        if (item.kind === 'string' && item.type === 'text/plain') {
-          item.getAsString((url) => {
-            this.uppy.log(`[URL] Adding file from pasted url: ${url}`)
-            this.addFile(url)
-          })
-        }
-      })
+    if (!e.clipboardData.items) {
+      return
     }
+    const items = toArray(e.clipboardData.items)
+
+    // When a file is pasted, it appears as two items: file name string, then
+    // the file itself; Url then treats file name string as URL, which is wrong.
+    // This makes sure Url ignores paste event if it contains an actual file
+    const hasFiles = items.filter(item => item.kind === 'file').length > 0
+    if (hasFiles) return
+
+    items.forEach((item) => {
+      if (item.kind === 'string' && item.type === 'text/plain') {
+        item.getAsString((url) => {
+          this.uppy.log(`[URL] Adding file from pasted url: ${url}`)
+          this.addFile(url)
+        })
+      }
+    })
   }
 
   render (state) {

+ 3 - 2
packages/@uppy/url/src/style.scss

@@ -7,6 +7,7 @@
   flex-direction: column;
   justify-content: center;
   align-items: center;
+  flex: 1;
 }
 
 .uppy-Url-input {
@@ -15,7 +16,7 @@
   margin-bottom: 15px;
 }
 
-  .uppy-Dashboard--wide .uppy-Url-input  {
+  .uppy-size--md .uppy-Url-input  {
     margin-bottom: 30px;
   }
 
@@ -23,6 +24,6 @@
   padding: 13px 25px;
 }
 
-  .uppy-Dashboard--wide .uppy-Url-importButton {
+  .uppy-size--md .uppy-Url-importButton {
     padding: 13px 35px;
   }

+ 1 - 1
packages/@uppy/webcam/src/CameraIcon.js

@@ -1,7 +1,7 @@
 const { h } = require('preact')
 
 module.exports = (props) => {
-  return <svg aria-hidden="true" class="UppyIcon" width="66" height="55" viewBox="0 0 66 55" xmlns="http://www.w3.org/2000/svg">
+  return <svg aria-hidden="true" fill="#0097DC" width="66" height="55" viewBox="0 0 66 55" xmlns="http://www.w3.org/2000/svg">
     <path d="M57.3 8.433c4.59 0 8.1 3.51 8.1 8.1v29.7c0 4.59-3.51 8.1-8.1 8.1H8.7c-4.59 0-8.1-3.51-8.1-8.1v-29.7c0-4.59 3.51-8.1 8.1-8.1h9.45l4.59-7.02c.54-.54 1.35-1.08 2.16-1.08h16.2c.81 0 1.62.54 2.16 1.08l4.59 7.02h9.45zM33 14.64c-8.62 0-15.393 6.773-15.393 15.393 0 8.62 6.773 15.393 15.393 15.393 8.62 0 15.393-6.773 15.393-15.393 0-8.62-6.773-15.393-15.393-15.393zM33 40c-5.648 0-9.966-4.319-9.966-9.967 0-5.647 4.318-9.966 9.966-9.966s9.966 4.319 9.966 9.966C42.966 35.681 38.648 40 33 40z" fill-rule="evenodd" />
   </svg>
 }

+ 2 - 3
packages/@uppy/webcam/src/PermissionsScreen.js

@@ -4,9 +4,8 @@ module.exports = (props) => {
   return (
     <div class="uppy-Webcam-permissons">
       <div class="uppy-Webcam-permissonsIcon">{props.icon()}</div>
-      <h1 class="uppy-Webcam-Title">Please allow access to your camera</h1>
-      <p>You have been prompted to allow camera access from this site.<br />
-      In order to take pictures with your camera you must approve this request.</p>
+      <h1 class="uppy-Webcam-title">{props.i18n('allowAccessTitle')}</h1>
+      <p>{props.i18n('allowAccessDescription')}</p>
     </div>
   )
 }

+ 7 - 3
packages/@uppy/webcam/src/index.js

@@ -39,7 +39,7 @@ module.exports = class Webcam extends Plugin {
     this.supportsUserMedia = !!this.mediaDevices
     this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
     this.id = this.opts.id || 'Webcam'
-    this.title = 'Camera'
+    this.title = this.opts.title || 'Camera'
     this.type = 'acquirer'
     this.icon = CameraIcon
 
@@ -48,7 +48,9 @@ module.exports = class Webcam extends Plugin {
         smile: 'Smile!',
         takePicture: 'Take a picture',
         startRecording: 'Begin video recording',
-        stopRecording: 'Stop video recording'
+        stopRecording: 'Stop video recording',
+        allowAccessTitle: 'Please allow access to your camera',
+        allowAccessDescription: 'In order to take pictures or record video with your camera, please allow camera access for this site.'
       }
     }
 
@@ -325,7 +327,9 @@ module.exports = class Webcam extends Plugin {
     const webcamState = this.getPluginState()
 
     if (!webcamState.cameraReady) {
-      return <PermissionsScreen icon={CameraIcon} />
+      return <PermissionsScreen
+        icon={CameraIcon}
+        i18n={this.i18n} />
     }
 
     return <CameraScreen

+ 24 - 15
packages/@uppy/webcam/src/style.scss

@@ -15,19 +15,16 @@
   flex-grow: 1;
   overflow: hidden;
   background-color: $color-black;
-  // height: 100%;
-  // display: flex;
-  // justify-content: center;
-  // align-items: center;
+  text-align: center;
 }
 
-  .uppy-Dashboard--wide .uppy-Webcam-videoContainer {
+  .uppy-size--md .uppy-Webcam-videoContainer {
     // height: initial;
   }
 
 .uppy-Webcam-video {
-  width: 100%;
-  height: 100%;
+  // width: 100%;
+  // height: 100%;
   max-width: 100%;
   max-height: 100%;
 }
@@ -56,7 +53,18 @@
   transition: all 0.3s;
 }
 
-  .uppy-Dashboard--wide .uppy-Webcam-button {
+  .uppy-Webcam-button svg {
+    width: 30px;
+    height: 30px;
+    max-width: 100%;
+    max-height: 100%;
+    display: inline-block;
+    vertical-align: text-top;
+    overflow: hidden;
+    fill: currentColor;
+  }
+
+  .uppy-size--md .uppy-Webcam-button {
     width: 60px;
     height: 60px;
   }
@@ -70,11 +78,6 @@
     box-shadow: 0 0 0 0.2rem rgba($color-cornflower-blue, 0.5);
   }
 
-  .uppy-Webcam-button .UppyIcon {
-    width: 30px;
-    height: 30px;
-  }
-
 .uppy-Webcam-button--picture {
   margin-right: 12px;
 }
@@ -86,9 +89,15 @@
   justify-content: center;
   flex-flow: column wrap;
   height: 100%;
+  flex: 1;
+}
+
+.uppy-Webcam-permissons p {
+  max-width: 450px;
+  line-height: 1.3;
 }
 
-.uppy-Webcam-Title {
+.uppy-Webcam-title {
   font-size: 22px;
   line-height: 1.35;
   font-weight: 400;
@@ -107,7 +116,7 @@
   margin: 0;
 }
 
-.uppy-Webcam-permissonsIcon .UppyIcon {
+.uppy-Webcam-permissonsIcon svg {
   width: 100px;
   height: 75px;
   color: lighten($color-gray, 15%);

+ 8 - 4
website/src/docs/aws-s3-multipart.md

@@ -6,7 +6,7 @@ module: "@uppy/aws-s3-multipart"
 permalink: docs/aws-s3-multipart/
 ---
 
-The `@uppy/aws-s3-multipart` plugin can be used to upload files directly to an S3 bucket using S3's Multipart upload strategy. With this strategy, files are chopped up in parts of 5MB+ each, so they can be uploaded concurrently. It's also very reliable: if a single part fails to upload, only that 5MB has to be retried.
+The `@uppy/aws-s3-multipart` plugin can be used to upload files directly to an S3 bucket using S3's Multipart upload strategy. With this strategy, files are chopped up in parts of 5MB+ each, so they can be uploaded concurrently. It is also very reliable: if a single part fails to upload, only that 5MB chunk has to be retried.
 
 ```js
 const AwsS3Multipart = require('@uppy/aws-s3-multipart')
@@ -20,6 +20,8 @@ uppy.use(AwsS3Multipart, {
 
 This plugin is published as the `@uppy/aws-s3-multipart` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/aws-s3-multipart
 ```
@@ -32,13 +34,15 @@ const AwsS3Multipart = Uppy.AwsS3Multipart
 
 ## Options
 
+The `@uppy/aws-s3-multipart` plugin has the following configurable options:
+
 ### limit: 0
 
-The maximum amount of chunks to upload simultaneously. `0` means unlimited.
+The maximum amount of chunks to upload simultaneously. Set to `0` to disable limiting.
 
 ### serverUrl: null
 
-The Uppy Server URL to use to proxy calls to the S3 Multipart API.
+The Uppy Server URL to use for proxying calls to the S3 Multipart API.
 
 ### createMultipartUpload(file)
 
@@ -85,7 +89,7 @@ Return a Promise for an object with keys:
      Key: partData.key,
      UploadId: partData.uploadId,
      PartNumber: partData.number,
-     Body: '', // Empty, because it's uploaded later
+     Body: '', // Empty, because it is uploaded later
      Expires: Date.now() + 5 * 60 * 1000
    }, (err, url) => { /* there's the url! */ })
    ```

+ 25 - 21
website/src/docs/aws-s3.md

@@ -7,7 +7,7 @@ permalink: docs/aws-s3/
 ---
 
 The `@uppy/aws-s3` plugin can be used to upload files directly to an S3 bucket.
-Uploads can be signed using [Uppy Server][uppy-server docs] or a custom signing function.
+Uploads can be signed using either [Uppy Server][uppy-server docs] or a custom signing function.
 
 ```js
 const AwsS3 = require('@uppy/aws-s3')
@@ -20,14 +20,16 @@ uppy.use(AwsS3, {
 })
 ```
 
-There are broadly two ways to upload to S3 in a browser. A server can generate a presigned URL for a [PUT upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html), or a server can generate form data for a [POST upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html). uppy-server uses a POST upload. See [POST uPloads](#post-uploads) for some caveats if you would like to use POST uploads without uppy-server. See [Generating a presigned upload URL server-side](#example-presigned-url) for an example of a PUT upload.
+There are broadly two ways of uploading to S3 in a browser. A server can generate a presigned URL for a [PUT upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html), or a server can generate form data for a [POST upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html). Uppy-server uses a POST upload. See [POST Uploads](#post-uploads) for some caveats if you would like to use POST uploads without uppy-server. See [Generating a presigned upload URL server-side](#example-presigned-url) for an example of a PUT upload.
 
-There is also a separate plugin for S3 Multipart uploads. Multipart in this sense is Amazon's proprietary chunked, resumable upload mechanism for large files. See the [`@uppy/aws-s3-multipart`](/docs/aws-s3-multipart) documentation.
+There is also a separate plugin for S3 Multipart uploads. Multipart in this sense refers to Amazon's proprietary chunked, resumable upload mechanism for large files. See the [`@uppy/aws-s3-multipart`](/docs/aws-s3-multipart) documentation.
 
 ## Installation
 
 This plugin is published as the `@uppy/aws-s3` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/aws-s3
 ```
@@ -40,6 +42,8 @@ const AwsS3 = Uppy.AwsS3
 
 ## Options
 
+The `@uppy/aws-s3` plugin has the following configurable options:
+
 ### `id: 'AwsS3'`
 
 A unique identifier for this plugin. Defaults to `'AwsS3'`.
@@ -58,18 +62,18 @@ uppy.use(AwsS3, {
 
 > Note: When using [uppy-server][uppy-server docs] to sign S3 uploads, do not define this option.
 
-A function returning upload parameters for a file.
+A function that returns upload parameters for a file.
 Parameters should be returned as an object, or a Promise for an object, with keys `{ method, url, fields, headers }`.
 
-The `method` field is the HTTP method to use for the upload.
-This should be one of `PUT` or `POST`, depending on the type of upload used.
+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 send the upload request to.
-When using a presigned PUT upload, this should be the URL to the S3 object including signing parameters in the query string.
+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 empty.
+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.
 
@@ -101,13 +105,13 @@ strings: {
 ## S3 Bucket configuration
 
 S3 buckets do not allow public uploads by default.
-In order to allow Uppy to upload to a bucket directly, its CORS permissions need to be configured.
+In order to allow Uppy 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.
 An XML document will be shown that contains the CORS configuration.
 
-Good practice is to use two CORS rules: one for viewing the uploaded files, and one for uploading files.
+It is good practice to use two CORS rules: one for viewing the uploaded files, and one for uploading files.
 
 Depending on which settings were enabled during bucket creation, AWS S3 may have defined a CORS rule that allows public reading already.
 This rule looks like:
@@ -149,7 +153,7 @@ When using a presigned upload URL, the following permissions must be granted:
 <AllowedMethod>PUT</AllowedMethod>
 ```
 
-The final configuration should look something like the below:
+The final configuration should look something like this:
 
 ```xml
 <?xml version="1.0" encoding="UTF-8"?>
@@ -174,7 +178,7 @@ The final configuration should look something like the below:
 
 In-depth documentation about CORS rules is available on the [AWS documentation site](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html).
 
-## POST Uploads
+## POST uploads
 
 uppy-server uses POST uploads by default, but you can also use them with your own endpoints. There are a few things to be aware of when doing so:
 
@@ -190,9 +194,9 @@ uppy-server uses POST uploads by default, but you can also use them with your ow
    })
    ```
 
-## S3 Alternatives
+## S3 alternatives
 
-Many other object storage providers have an identical API to S3, so you can use the `@uppy/aws-s3` plugin with them. To use them with Uppy Server, you can set the `UPPYSERVER_AWS_ENDPOINT` variable to the endpoint of your preferred service.
+Many other object storage providers have an identical API to S3, so you can use the `@uppy/aws-s3` plugin with them as well. To use them with Uppy Server, you can set the `UPPYSERVER_AWS_ENDPOINT` variable to the endpoint of your preferred service.
 
 ### DigitalOcean Spaces
 
@@ -209,7 +213,7 @@ For a working example that you can run and play around with, see the [digitaloce
 
 ### Google Cloud Storage
 
-For Google Cloud Storage, you need to take a few more steps. 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 Uppy Server like below:
+For Google Cloud Storage, you need to take a few more steps. 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 Uppy Server like this:
 
 ```bash
 export UPPYSERVER_AWS_ENDPOINT="https://storage.googleapis.com"
@@ -220,11 +224,11 @@ export UPPYSERVER_AWS_SECRET="YOUR-GCS-SECRET" # The Secret
 
 You do not need to configure the region with GCS.
 
-You also need to configure CORS differently. Unlike Amazon, Google does not offer a UI for CORS configurations. Instead an HTTP API must be used. 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 below steps to do it using Google's API playground.
+You also need to configure CORS differently. Unlike Amazon, Google does not offer a UI for CORS configurations. Instead, an HTTP API must be used. 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.
 
-GCS has multiple CORS formats, both XML and JSON. Unfortunately their XML format is different from Amazon's, so we can't simply use the one from the [S3 Bucket configuration](#S3-Bucket-configuration) section. Google appears to favour the JSON format, so we'll use that.
+GCS has multiple CORS formats, both XML and JSON. Unfortunately, their XML format is different from Amazon's, so we can't simply use the one from the [S3 Bucket configuration](#S3-Bucket-configuration) section. Google appears to favour the JSON format, so we will use that.
 
-#### JSON CORS Configuration
+#### JSON CORS configuration
 
 The JSON format consists of an array of CORS configuration objects. An example using POST policy document uploads is shown here:
 
@@ -259,7 +263,7 @@ Otherwise, you can manually apply it through the OAuth playground:
    1. Select the "Cloud Storage JSON API v1" » "devstorage.full_control" scope
    1. Press "Authorize APIs" and allow access
  1. Click "Step 3 - Configure request to API"
- 1. Configure it like below:
+ 1. Configure it as follows:
    - HTTP Method: PATCH
    - Request URI: `https://www.googleapis.com/storage/v1/b/YOUR_BUCKET_NAME`
    - Content-Type: application/json (should be the default)
@@ -309,7 +313,7 @@ See the [aws-presigned-url example in the uppy repository](https://github.com/tr
 
 ### Retrieving presign parameters of the uploaded file
 
-Once the file is uploaded, it's possible to retrieve the parameters that were
+Once the file is uploaded, it is possible to retrieve the parameters that were
 generated in `getUploadParameters(file)` via the `file.meta` field:
 
 ```js

+ 11 - 3
website/src/docs/dashboard.md

@@ -43,7 +43,7 @@ const Dashboard = Uppy.Dashboard
 
 ## CSS
 
-The Dashboard plugin includes CSS for the Dashboard itself, and the plugins the Dashboard uses ([`@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.
+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.
 
 The CSS file lives at `@uppy/dashboard/dist/style.css`. A minified version is at `@uppy/dashboard/dist/style.min.css`.
 
@@ -81,7 +81,7 @@ uppy.use(Dashboard, {
 
 ### `id: 'Dashboard'`
 
-A unique identifier for this Dashboard. It defaults to `'Dashboard'`, but you can change this if you need multiple Dashboard instances.
+A unique identifier for this plugin. It defaults to `'Dashboard'`, but you can change this if you need multiple Dashboard instances.
 Plugins that are added by the Dashboard get unique IDs based on this ID, like `'Dashboard:StatusBar'` and `'Dashboard:Informer'`.
 
 ### `target: 'body'`
@@ -230,6 +230,8 @@ strings: {
   editFile: 'Edit file',
   // Shown in the panel header for the metadata editor. Rendered as "Editing image.png".
   editing: 'Editing %{file}',
+  // Text for a button shown on the file preview, used to edit file metadata
+  edit: 'Edit',
   // Used as the screen reader label for the button that saves metadata edits and returns to the
   // file list view.
   finishEditingFile: 'Finish editing file',
@@ -238,7 +240,7 @@ strings: {
   // Shown in the main dashboard area when no files have been selected, and one or more
   // remote provider plugins are in use. %{browse} is replaced with a link that opens the system
   // file selection dialog.
-  dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}',
+  dropPasteImport: 'Drop files here, paste, %{browse} or import from',
   // Shown in the main dashboard area when no files have been selected, and no provider
   // plugins are in use. %{browse} is replaced with a link that opens the system
   // file selection dialog.
@@ -255,6 +257,12 @@ strings: {
   // Used as the hover text and screen reader label for the buttons to retry failed uploads.
   retryUpload: 'Retry upload',
 
+  // Used in a title, how many files are currently selected
+  xFilesSelected: {
+    0: '%{smart_count} file selected',
+    1: '%{smart_count} files selected'
+  },
+
   // @uppy/status-bar strings:
   uploading: 'Uploading',
   complete: 'Complete'

+ 1 - 1
website/src/docs/dragdrop.md

@@ -45,7 +45,7 @@ Import one of these files into your project. The way to do this depends on your
 
 ## Options
 
-The DragDrop plugin has the following configurable options:
+The `@uppy/drag-drop` plugin has the following configurable options:
 
 ```js
 uppy.use(DragDrop, {

+ 11 - 3
website/src/docs/dropbox.md

@@ -6,7 +6,7 @@ module: "@uppy/dropbox"
 permalink: docs/dropbox/
 ---
 
-The `@uppy/dropbox` plugin lets users import files their Dropbox account.
+The `@uppy/dropbox` plugin lets users import files from their Dropbox account.
 
 An Uppy Server instance is required for the Dropbox plugin to work. Uppy Server 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.
 
@@ -24,6 +24,8 @@ uppy.use(Dropbox, {
 
 This plugin is published as the `@uppy/dropbox` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/dropbox
 ```
@@ -36,6 +38,8 @@ const Dropbox = Uppy.Dropbox
 
 ## Options
 
+The `@uppy/dropbox` plugin has the following configurable options:
+
 ```js
 uppy.use(Dropbox, {
   target: Dashboard,
@@ -45,7 +49,11 @@ uppy.use(Dropbox, {
 
 ### `id: 'Dropbox'`
 
-A unique identifier for this plugin. Defaults to `'Dropbox'`.
+A unique identifier for this plugin. It defaults to `'Dropbox'`.
+
+### `title: 'Dropbox'`
+
+Title / name shown in the UI, such as Dashboard tabs. It defaults to `'Dropbox'`.
 
 ### `target: null`
 
@@ -65,7 +73,7 @@ The valid and authorised URL(s) from which OAuth responses should be accepted.
 
 This value can be a `String`, a `Regex` pattern, or an `Array` of both.
 
-This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise the default value should do just fine.
+This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise, the default value should do just fine.
 
 ### `locale: {}`
 

+ 3 - 3
website/src/docs/fileinput.md

@@ -40,7 +40,7 @@ const FileInput = Uppy.FileInput
 
 ## CSS
 
-The FileInput plugin includes some simple 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.
+The `@uppy/file-input` plugin includes some simple 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.
 
 The CSS file lives at `@uppy/file-input/dist/style.css`. A minified version is at `@uppy/file-input/dist/style.min.css`.
 
@@ -48,7 +48,7 @@ Import one of these files into your project. The way to do this depends on your
 
 ## Options
 
-The FileInput plugin has the following configurable options:
+The `@uppy/file-input` plugin has the following configurable options:
 
 ```js
 uppy.use(FileInput, {
@@ -64,7 +64,7 @@ uppy.use(FileInput, {
 
 ### `id: 'FileInput'`
 
-A unique identifier for this File Input. It defaults to `'FileInput'`. Use this if you need to add multiple FileInput instances.
+A unique identifier for this plugin. It defaults to `'FileInput'`. Use this if you need to add multiple FileInput instances.
 
 ### `target: null`
 

+ 2 - 2
website/src/docs/form.md

@@ -37,7 +37,7 @@ const Form = Uppy.Form
 
 ## Options
 
-The Form plugin has the following configurable options:
+The `@uppy/form` plugin has the following configurable options:
 
 ```js
 uppy.use(Form, {
@@ -51,7 +51,7 @@ uppy.use(Form, {
 
 ### `id: 'Form'`
 
-A unique identifier for this Form. It defaults to `'Form'`.
+A unique identifier for this plugin. It defaults to `'Form'`.
 
 ### `target: null`
 

+ 6 - 4
website/src/docs/golden-retriever.md

@@ -6,14 +6,16 @@ permalink: docs/golden-retriever/
 order: 71
 ---
 
-The `@uppy/golden-retriever` plugin saves selected files in your browser cache, so that if the browser crashes, Uppy can restore everything and continue uploading like nothing happened. Read more about it [on the blog](https://uppy.io/blog/2017/07/golden-retriever/).
+The `@uppy/golden-retriever` plugin saves selected files in your browser cache, so that if the browser crashes, 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 LocalStorage to store file metadata and Uppy state, and IndexedDB for small files. It also uses a Service Worker for _all_ filesunlike IndexedDB, a Service Worker can keep very large files. Service Worker storage is _very_ temporary though, and doesn't persist across browser crashes or restarts. It works very well for accidental refreshes or closed tabs.
+The Golden Retriever uses LocalStorage to store file metadata and Uppy state, and IndexedDB for small files. It also uses a Service Worker for _all_ files because, unlike IndexedDB, a Service Worker can keep very large files. Service Worker storage is _very_ temporary though, and doesn't persist across browser crashes or restarts. It works very well, however, for accidental refreshes or closed tabs.
 
 ## Installation
 
 This plugin is published as the `@uppy/golden-retriever` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/golden-retriever
 ```
@@ -26,7 +28,7 @@ const GoldenRetriever = Uppy.GoldenRetriever
 
 ## Usage
 
-1\. Bundle your own service worker `sw.js` file with Uppy GoldenRetriever’s service worker. If you’re using Browserify, just bundle it separately, for Webpack there is a plugin [serviceworker-webpack-plugin](https://github.com/oliviertassinari/serviceworker-webpack-plugin).
+1\. Bundle your own service worker `sw.js` file with Uppy GoldenRetriever’s service worker. If you are using Browserify, just bundle it separately. For Webpack, there is a plugin [serviceworker-webpack-plugin](https://github.com/oliviertassinari/serviceworker-webpack-plugin).
 
 ```js
 // sw.js
@@ -54,4 +56,4 @@ if ('serviceWorker' in navigator) {
 }
 ```
 
-Voila, that’s it. Happy retrieving!
+Voilà, that’s it. Happy retrieving!

+ 10 - 2
website/src/docs/google-drive.md

@@ -24,6 +24,8 @@ uppy.use(GoogleDrive, {
 
 This plugin is published as the `@uppy/google-drive` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/google-drive
 ```
@@ -36,6 +38,8 @@ const GoogleDrive = Uppy.GoogleDrive
 
 ## Options
 
+The `@uppy/google-drive` plugin has the following configurable options:
+
 ```js
 uppy.use(GoogleDrive, {
   target: Dashboard,
@@ -45,7 +49,11 @@ uppy.use(GoogleDrive, {
 
 ### `id: 'GoogleDrive'`
 
-A unique identifier for this plugin. Defaults to `'GoogleDrive'`.
+A unique identifier for this plugin. It defaults to `'GoogleDrive'`.
+
+### `title: 'Google Drive'`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs. It defaults to `'Google Drive'`.
 
 ### `target: null`
 
@@ -65,7 +73,7 @@ The valid and authorised URL(s) from which OAuth responses should be accepted.
 
 This value can be a `String`, a `Regex` pattern, or an `Array` of both.
 
-This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise the default value should be good enough.
+This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise, the default value should be good enough.
 
 ### `locale: {}`
 

+ 3 - 3
website/src/docs/informer.md

@@ -40,7 +40,7 @@ const Informer = Uppy.Informer
 
 ## CSS
 
-The Informer plugin includes CSS a file for styling. If you use the [`@uppy/dashboard`](/docs/dashboard) plugin, you do not need to include the styles for the Informer, because the Dashboard already includes it.
+The `@uppy/informer` plugin includes CSS a file for styling. If you use the [`@uppy/dashboard`](/docs/dashboard) plugin, you do not need to include the styles for the Informer, because the Dashboard already includes it.
 
 The CSS file lives at `@uppy/informer/dist/style.css`. A minified version is at `@uppy/informer/dist/style.min.css`.
 
@@ -48,11 +48,11 @@ Import one of these files into your project. The way to do this depends on your
 
 ## Options
 
-The Informer plugin has the following configurable options:
+The `@uppy/informer` plugin has the following configurable options:
 
 ### `id: 'Informer'`
 
-A unique identifier for this Informer. It defaults to `'Informer'`. Use this if you need multiple Informer instances.
+A unique identifier for this plugin. It defaults to `'Informer'`. Use this if you need multiple Informer instances.
 
 ### `target: null`
 

+ 11 - 3
website/src/docs/instagram.md

@@ -6,7 +6,7 @@ module: "@uppy/instagram"
 permalink: docs/instagram/
 ---
 
-The `@uppy/instagram` plugin lets users import files their Instagram account.
+The `@uppy/instagram` plugin lets users import files from their Instagram account.
 
 An Uppy Server instance is required for the `@uppy/instagram` plugin to work. Uppy Server handles authentication with Instagram, downloads the pictures and videos, and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
 
@@ -26,6 +26,8 @@ uppy.use(Instagram, {
 
 This plugin is published as the `@uppy/instagram` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/instagram
 ```
@@ -38,6 +40,8 @@ const Instagram = Uppy.Instagram
 
 ## Options
 
+The `@uppy/instagram` plugin has the following configurable options:
+
 ```js
 uppy.use(Instagram, {
   target: Dashboard,
@@ -47,7 +51,11 @@ uppy.use(Instagram, {
 
 ### `id: 'Instagram'`
 
-A unique identifier for this plugin. Defaults to `'Instagram'`.
+A unique identifier for this plugin. It defaults to `'Instagram'`.
+
+### `title: 'Instagram'`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs. It defaults to `'Instagram'`.
 
 ### `target: null`
 
@@ -67,7 +75,7 @@ The valid and authorised URL(s) from which OAuth responses should be accepted.
 
 This value can be a `String`, a `Regex` pattern, or an `Array` of both.
 
-This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise the default value should be good enough.
+This is useful when you have your [Uppy Server](/docs/server) running on multiple hosts. Otherwise, the default value should be good enough.
 
 ### `locale: {}`
 

+ 3 - 3
website/src/docs/progressbar.md

@@ -39,7 +39,7 @@ const ProgressBar = Uppy.ProgressBar
 
 ## Options
 
-The Progressbar plugin has the following configurable options:
+The `@uppy/progress-bar` plugin has the following configurable options:
 
 ```js
 uppy.use(ProgressBar, {
@@ -59,7 +59,7 @@ DOM element, CSS selector, or plugin to mount the progress bar into.
 
 ### `fixed: false`
 
-When true, show the progress bar at the top of the page with `position: fixed`. When false, show the progress bar inline wherever it is mounted.
+When set to true, show the progress bar at the top of the page with `position: fixed`. When set to false, show the progress bar inline wherever it is mounted.
 
 ```js
 uppy.use(ProgressBar, {
@@ -70,7 +70,7 @@ uppy.use(ProgressBar, {
 
 ### `hideAfterFinish: true`
 
-When true, hides the progress bar after the upload has finished. If false, it remains visible.
+When set to true, hides the progress bar after the upload has finished. If set to false, it remains visible.
 
 ### `replaceTargetContent: false`
 

+ 6 - 4
website/src/docs/providers.md

@@ -5,11 +5,13 @@ permalink: docs/providers/
 order: 30
 ---
 
-The Provider plugins help you connect to your accounts with remote file providers such as [Dropbox](https://dropbox.com), [Google Drive](https://drive.google.com), [Instagram](https://instagram.com) and remote urls (import a file by pasting a direct link to it). Because this requires server to server communication, they work tightly with [uppy-server](https://github.com/transloadit/uppy-server) to manage the server to server authorization for your account. Almost all of the communication (file download/upload) is done on the server-to-server end, so this saves you the stress and bills of data consumption on the client.
+The Provider plugins help you connect to your accounts with remote file providers such as [Dropbox](https://dropbox.com), [Google Drive](https://drive.google.com), [Instagram](https://instagram.com) and remote URLs (importing a file by pasting a direct link to it). Because this requires server-to-server communication, they work tightly with [uppy-server](https://github.com/transloadit/uppy-server) to manage the server-to-server authorization for your account. Almost all of the communication (file download/upload) is done on the server-to-server end, so this saves you the stress and bills of data consumption on the client.
 
-As of now, the supported providers are [**Dropbox**](/docs/dropbox), [**GoogleDrive**](/docs/google-drive), [**Instagram**](/docs/instagram), and [**Url**](/docs/url).
+As of now, the supported providers are [**Dropbox**](/docs/dropbox), [**GoogleDrive**](/docs/google-drive), [**Instagram**](/docs/instagram), and [**URL**](/docs/url).
 
-Usage of the Provider plugins is not that different from any other *acquirer* plugin, except that it takes an extra option `serverUrl`, which specifies the url to your running `uppy-server`. This allows Uppy to know what server to connect to when server related operations are required by the provider plugin. Here's a quick example.
+Usage of the Provider plugins is not that different from any other *acquirer* plugin, except that it takes an extra option `serverUrl`, which specifies the URL to the `uppy-server` that you are running. This allows Uppy to know what server to connect to when server-related operations are required by the provider plugin. 
+
+Here's a quick example:
 
 ```js
 const Uppy = require('@uppy/core')
@@ -31,7 +33,7 @@ uppy.use(Dropbox, {target: Dashboard, serverUrl: 'http://localhost:3020'})
 const Instagram = require('@uppy/instagram')
 uppy.use(Instagram, {target: Dashboard, serverUrl: 'http://localhost:3020'})
 
-// for Url
+// for URL
 const Url = require('@uppy/url')
 uppy.use(Url, {target: Dashboard, serverUrl: 'http://localhost:3020'})
 ```

+ 4 - 2
website/src/docs/react-dashboard-modal.md

@@ -9,6 +9,8 @@ The `<DashboardModal />` component wraps the [`@uppy/dashboard`][] plugin, allow
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```
@@ -30,7 +32,7 @@ On top of all the [`@uppy/dashboard`][] options, the `<DashboardModal />` plugin
 To use other plugins like [`@uppy/webcam`][] with the `<DashboardModal />` component, add them to the Uppy instance and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
 
 ```js
-// Do this wherever you initialize Uppy, eg. in a React component's constructor method.
+// Do this wherever you initialize Uppy, e.g., in a React component's constructor method.
 // Do NOT do it in `render()` or any other method that is called more than once!
 uppy.use(Webcam) // `id` defaults to "Webcam"
 uppy.use(Webcam, { id: 'MyWebcam' }) // `id` is… "MyWebcam"
@@ -38,7 +40,7 @@ uppy.use(Webcam, { id: 'MyWebcam' }) // `id` is… "MyWebcam"
 
 Then do `plugins={['Webcam']}`.
 
-A full example that uses a button to open the modal is shown below:
+Here is a full example that uses a button to open the modal:
 
 ```js
 class MusicUploadButton extends React.Component {

+ 5 - 3
website/src/docs/react-dashboard.md

@@ -9,6 +9,8 @@ The `<Dashboard />` component wraps the [`@uppy/dashboard`][] plugin. It only re
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```
@@ -22,16 +24,16 @@ import { Dashboard } from '@uppy/react'
 
 The `<Dashboard />` component supports all [`@uppy/dashboard`][] options as props.
 
-The `<Dashboard />` cannot be passed to a `target:` option of a remote provider or plugins like [`@uppy/webcam`][]. To use other plugins like [`@uppy/webcam`][] with the `<Dashboard />` component, first add them to the Uppy instance, and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
+The `<Dashboard />` cannot be passed to a `target:` option of a remote provider or plugins such as [`@uppy/webcam`][]. To use other plugins like [`@uppy/webcam`][] with the `<Dashboard />` component, first add them to the Uppy instance, and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
 
 ```js
-// Do this wherever you initialize Uppy, eg. in a React component's constructor method.
+// Do this wherever you initialize Uppy, e.g., in a React component's constructor method.
 // Do NOT do it in `render()` or any other method that is called more than once!
 uppy.use(Webcam) // `id` defaults to "Webcam"
 uppy.use(Webcam, { id: 'MyWebcam' }) // `id` is… "MyWebcam"
 ```
 
-Then in `render()` do:
+Then add the following to `render()`:
 
 ```js
 <Dashboard

+ 2 - 0
website/src/docs/react-dragdrop.md

@@ -10,6 +10,8 @@ The `<DragDrop />` component wraps the [`@uppy/drag-drop`][] plugin.
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```

+ 2 - 0
website/src/docs/react-progressbar.md

@@ -10,6 +10,8 @@ The `<ProgressBar />` component wraps the [`@uppy/progress-bar`][] plugin.
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```

+ 2 - 0
website/src/docs/react-statusbar.md

@@ -10,6 +10,8 @@ The `<StatusBar />` component wraps the [`@uppy/status-bar`][] plugin.
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```

+ 4 - 2
website/src/docs/react.md

@@ -11,13 +11,15 @@ Uppy provides [React][] components for the included UI plugins.
 
 All React components are provided through the `@uppy/react` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/react
 ```
 
 ## Usage
 
-The components can be used with [React][] or API-compatible alternatives such as [Preact][].
+The components can be used with either [React][] or API-compatible alternatives such as [Preact][].
 
 Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy instance can be passed into components as an `uppy` prop.
 All other props are passed as options to the plugin.
@@ -60,7 +62,7 @@ const AvatarPicker = ({ currentAvatar }) => {
 }
 ```
 
-The plugins that are available as React component wrappers are:
+The following plugins are available as React component wrappers:
 
  - [&lt;Dashboard />][] - renders an inline [`@uppy/dashboard`][]
  - [&lt;DashboardModal />][] - renders a [`@uppy/dashboard`][] modal

+ 4 - 4
website/src/docs/redux.md

@@ -5,11 +5,11 @@ permalink: docs/redux/
 order: 87
 ---
 
-Uppy supports popular [Redux](https://redux.js.org/) state management library in two ways:
+Uppy supports the popular [Redux](https://redux.js.org/) state management library in two ways:
 
 ## Redux Store
 
-You can tell Uppy to use your app’s Redux store for its files and UI state. Please checkout [Custom Stores](/docs/stores/) for more info on that. Here’s an example to give you a sense of how this works:
+You can tell Uppy to use your app’s Redux store for its files and UI state. Please check out [Custom Stores](/docs/stores/) for more information on that. Here’s an example to give you a sense of how this works:
 
 ```js
 const { createStore } = require('redux')
@@ -29,7 +29,7 @@ const uppy = Uppy({
 
 ## Redux Dev Tools
 
-`ReduxDevTools` plugin that simply syncs with [redux-devtools](https://github.com/gaearon/redux-devtools) browser or JS extensions, and allows for basic time travel:
+This is a `ReduxDevTools` plugin that simply syncs with the [redux-devtools](https://github.com/gaearon/redux-devtools) browser or JS extensions, and allows for basic time travel:
 
 ```js
 const Uppy = require('@uppy/core')
@@ -48,4 +48,4 @@ const uppy = Uppy({
 
 After you `.use(ReduxDevTools)`, you should be able to see Uppy’s state in Redux Dev Tools.
 
-You likely don’t need this if you are actually using Redux yourself and Redux Store in Uppy from above, since it will just work.
+You will likely not need this if you are actually using Redux yourself, as well as Redux Store in Uppy like in the example above, since it will just work automatically in that case.

+ 2 - 0
website/src/docs/server.md

@@ -23,6 +23,8 @@ As of now, Uppy Server is integrated to work with:
 
 ## Installation
 
+Install from NPM:
+
 ```bash
 npm install uppy-server
 ```

+ 4 - 4
website/src/docs/statusbar.md

@@ -8,7 +8,7 @@ alias: docs/statusbar/
 ---
 
 The `@uppy/status-bar` plugin shows upload progress and speed, ETAs, pre- and post-processing information, and allows users to control (pause/resume/cancel) the upload.
-Best used in combination with with a simple file source plugin, such as [`@uppy/file-input`][] or [`@uppy/drag-drop`][], or a custom implementation.
+It is best used in combination with a simple file source plugin, such as [`@uppy/file-input`][] or [`@uppy/drag-drop`][], or a custom implementation.
 
 ```js
 const StatusBar = require('@uppy/status-bar')
@@ -38,15 +38,15 @@ const StatusBar = Uppy.StatusBar
 
 ## CSS
 
-The StatusBar plugin includes CSS a file for styling. If you use the [`@uppy/dashboard`](/docs/dashboard) plugin, you do not need to include the styles for the StatusBar, because the Dashboard already includes it.
+The `@uppy/status-bar` plugin includes a CSS file for styling. If you are using the [`@uppy/dashboard`](/docs/dashboard) plugin, you do not need to include the styles for the StatusBar, because the Dashboard already includes it.
 
-The CSS file lives at `@uppy/status-bar/dist/style.css`. A minified version is at `@uppy/status-bar/dist/style.min.css`.
+The CSS file lives at `@uppy/status-bar/dist/style.css`. A minified version can be found at `@uppy/status-bar/dist/style.min.css`.
 
 Import one of these files into your project. The way to do this depends on your build system.
 
 ## Options
 
-The StatusBar plugin has the following configurable options:
+The `@uppy/status-bar` plugin has the following configurable options:
 
 ```js
 uppy.use(StatusBar, {

+ 19 - 15
website/src/docs/transloadit.md

@@ -6,7 +6,7 @@ module: "@uppy/transloadit"
 permalink: docs/transloadit/
 ---
 
-The `@uppy/transloadit` plugin can be used to upload files to [Transloadit](https://transloadit.com/) for all kinds of processing, such as transcoding video, resizing images, zipping/unzipping, [and more](https://transloadit.com/services/).
+The `@uppy/transloadit` plugin can be used to upload files to [Transloadit](https://transloadit.com/) for all kinds of processing, such as transcoding video, resizing images, zipping/unzipping, [and much more](https://transloadit.com/services/).
 
 <a class="TryButton" href="/examples/transloadit/">Try it live</a>
 
@@ -25,12 +25,14 @@ uppy.use(Transloadit, {
 })
 ```
 
-As of Uppy 0.24 the Transloadit plugin includes the [Tus](/docs/tus) plugin to handle the uploading, so you no longer have to add it manually.
+As of Uppy version 0.24, the Transloadit plugin includes the [Tus](/docs/tus) plugin to handle the uploading, so you no longer have to add it manually.
 
 ## Installation
 
 This plugin is published as the `@uppy/transloadit` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/transloadit
 ```
@@ -66,17 +68,19 @@ uppy.use(Dropbox, {
 
 ## Options
 
+The `@uppy/transloadit` plugin has the following configurable options:
+
 ### `id: 'Transloadit'`
 
-A unique identifier for this plugin. Defaults to `'Transloadit'`.
+A unique identifier for this plugin. It defaults to `'Transloadit'`.
 
 ### `service`
 
-The Transloadit API URL to use. Defaults to `https://api2.transloadit.com`, which will attempt to route traffic efficiently based on where your users are. You can set this to something like `https://api2-us-east-1.transloadit.com` if you want to use a particular region.
+The Transloadit API URL to use. It defaults to `https://api2.transloadit.com`, which will attempt to route traffic efficiently based on the location of your users. You can set this to something like `https://api2-us-east-1.transloadit.com` if you want to use a particular region.
 
 ### `params`
 
-The Assembly parameters to use for the upload. See the Transloadit documentation on [Assembly Instructions](https://transloadit.com/docs/#14-assembly-instructions). `params` should be a plain JavaScript object, or a JSON string if you are using the [`signature`](#signature) option.
+The Assembly parameters to use for the upload. See the Transloadit documentation on [Assembly Instructions](https://transloadit.com/docs/#14-assembly-instructions) for further information. `params` should be a plain JavaScript object, or a JSON string if you are using the [`signature`](#signature) option.
 
 The `auth.key` Assembly parameter is required. You can also use the `steps` or `template_id` options here as described in the Transloadit documentation.
 
@@ -101,15 +105,15 @@ uppy.use(Transloadit, {
 
 ### `waitForEncoding`
 
-Whether to wait for all Assemblies to complete before completing the upload.
+Configures whether or not to wait for all Assemblies to complete before completing the upload.
 
 ### `waitForMetadata`
 
-Whether to wait for metadata to be extracted from uploaded files before completing the upload. If `waitForEncoding` is enabled, this has no effect.
+Configures whether or not to wait for metadata to be extracted from uploaded files before completing the upload. If `waitForEncoding` is enabled, this has no effect.
 
 ### `importFromUploadURLs`
 
-Instead of uploading to Transloadit's servers directly, allow another plugin to upload files, and then import those files into the Transloadit Assembly. Default `false`.
+Instead of uploading to Transloadit's servers directly, allow another plugin to upload files, and then import those files into the Transloadit Assembly. This is set to `false` by default.
 
 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.
 
@@ -130,15 +134,15 @@ uppy.use(Transloadit, {
 })
 ```
 
-In order for this to work, the upload plugin must assign a publically accessible `uploadURL` property to the uploaded file object. The Tus and S3 plugins both do this—for the XHRUpload plugin, you may have to specify a custom `getUploadResponse` function.
+In order for this to work, the upload plugin must assign a publically accessible `uploadURL` property to the uploaded file object. The Tus and S3 plugins both do this automatically. For the XHRUpload plugin, you may have to specify a custom `getUploadResponse` function.
 
 ### `alwaysRunAssembly`
 
-When true, always create and run an Assembly when `uppy.upload()` is called, even if no files were selected. 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.
+When set to true, always create and run an Assembly when `uppy.upload()` is called, even if no files were selected. 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.
 
 ### `signature`
 
-An optional signature for the Assembly parameters. See the Transloadit documentation on [Signature Authentication](https://transloadit.com/docs/#26-signature-authentication).
+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.
 
@@ -239,7 +243,7 @@ strings: {
   // 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 set.
+  // This only shows if `waitForMetadata` or `waitForEncoding` was enabled.
   encoding: 'Encoding...'
 }
 ```
@@ -272,15 +276,15 @@ Fired when Transloadit has received an upload.
 **Parameters**
 
   - `file` - The Transloadit file object that was uploaded.
-  - `assembly` - The [Assembly Status][assembly-status] of the Assembly the file was uploaded to.
+  - `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 now executing the Assembly.
+Fired when Transloadit has received all uploads, and is currently executing the Assembly.
 
 **Parameters**
 
- - `assembly` - The [Assembly Status](https://transloadit.com/docs/api-docs/#assembly-status-response) of the Assembly that is now executing.
+ - `assembly` - The [Assembly Status](https://transloadit.com/docs/api-docs/#assembly-status-response) of the Assembly that is currently executing.
 
 ### `transloadit:result`
 

+ 4 - 0
website/src/docs/uppy.md

@@ -16,6 +16,8 @@ const uppy = Uppy()
 
 ## Installation
 
+Install from NPM:
+
 ```shell
 npm install @uppy/core
 ```
@@ -28,6 +30,8 @@ const Core = Uppy.Core
 
 ## Options
 
+The Uppy core module has the following configurable options:
+
 ```js
 const uppy = Uppy({
   id: 'uppy',

+ 12 - 4
website/src/docs/url.md

@@ -6,9 +6,9 @@ module: "@uppy/url"
 permalink: docs/url/
 ---
 
-The `@uppy/url` plugin lets users import files from the Internet. Paste any URL and it'll be added!
+The `@uppy/url` plugin allows users to import files from the internet. Paste any URL and it will be added!
 
-An Uppy Server instance is required for the `@uppy/url` plugin to work. Uppy Server will download the files and upload them to their destination. This saves bandwidth for the user (especially on mobile connections) and helps avoid CORS restrictions.
+An Uppy Server instance is required for the `@uppy/url` plugin to work. Uppy Server will download the files and upload them to their destination. This saves bandwidth for the user (especially on mobile connections) and helps to avoid CORS restrictions.
 
 ```js
 const Url = require('@uppy/url')
@@ -24,6 +24,8 @@ uppy.use(Url, {
 
 This plugin is published as the `@uppy/url` package.
 
+Install from NPM:
+
 ```shell
 npm install @uppy/url
 ```
@@ -36,6 +38,8 @@ const Url = Uppy.Url
 
 ## Options
 
+The `@uppy/url` plugin has the following configurable options:
+
 ```js
 uppy.use(Url, {
   target: Dashboard,
@@ -46,11 +50,15 @@ uppy.use(Url, {
 
 ### `id: 'Url'`
 
-A unique identifier for this plugin. Defaults to `'Url'`.
+A unique identifier for this plugin. It defaults to `'Url'`.
+
+### `title: 'Link'`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs. It defaults to `'Link'`.
 
 ### `target: null`
 
-DOM element, CSS selector, or plugin to mount the Url provider into. This should normally be the Dashboard.
+DOM element, CSS selector, or plugin to mount the URL provider into. This should normally be the Dashboard.
 
 ### `serverUrl: null`
 

+ 9 - 1
website/src/docs/webcam.md

@@ -38,7 +38,7 @@ const Webcam = Uppy.Webcam
 
 ## Options
 
-The Webcam plugin has the following configurable options:
+The `@uppy/webcam` plugin has the following configurable options:
 
 ```js
 uppy.use(Webcam, {
@@ -60,6 +60,10 @@ uppy.use(Webcam, {
 
 A unique identifier for this plugin. It defaults to `'Webcam'`.
 
+### `title: 'Camera'`
+
+Configures the title / name shown in the UI, for instance, on Dashboard tabs. It defaults to `'Camera'`.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount Webcam into.
@@ -116,5 +120,9 @@ strings: {
   // 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',
+  // 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.'
 }
 ```

+ 1 - 1
website/src/docs/xhrupload.md

@@ -37,7 +37,7 @@ const XHRUpload = Uppy.XHRUpload
 
 ## Options
 
-The XHRUpload plugin has the following configurable options:
+The `@uppy/xhr-upload` plugin has the following configurable options:
 
 ### `id: 'XHRUpload'`