Browse Source

Merge pull request #942 from transloadit/feature/dashboard-ui-notabs

Dashboard UI: no tabs, AddFiles panel, new breakpoints, refactors and fixes
Artur Paikin 6 years ago
parent
commit
08831ddad9
52 changed files with 1150 additions and 843 deletions
  1. 8 52
      packages/@uppy/core/src/_common.scss
  2. 1 1
      packages/@uppy/core/src/_variables.scss
  3. 4 2
      packages/@uppy/dashboard/package.json
  4. 0 100
      packages/@uppy/dashboard/src/FileCard.js
  5. 0 60
      packages/@uppy/dashboard/src/FileList.js
  6. 0 78
      packages/@uppy/dashboard/src/Tabs.js
  7. 2 2
      packages/@uppy/dashboard/src/components/ActionBrowseTagline.js
  8. 98 0
      packages/@uppy/dashboard/src/components/AddFiles.js
  9. 21 0
      packages/@uppy/dashboard/src/components/AddFilesPanel.js
  10. 35 27
      packages/@uppy/dashboard/src/components/Dashboard.js
  11. 111 0
      packages/@uppy/dashboard/src/components/FileCard.js
  12. 31 31
      packages/@uppy/dashboard/src/components/FileItem.js
  13. 0 0
      packages/@uppy/dashboard/src/components/FileItemProgress.js
  14. 23 0
      packages/@uppy/dashboard/src/components/FileList.js
  15. 1 1
      packages/@uppy/dashboard/src/components/FilePreview.js
  16. 28 0
      packages/@uppy/dashboard/src/components/PanelContent.js
  17. 31 0
      packages/@uppy/dashboard/src/components/PanelTopBar.js
  18. 4 18
      packages/@uppy/dashboard/src/components/icons.js
  19. 47 9
      packages/@uppy/dashboard/src/index.js
  20. 272 207
      packages/@uppy/dashboard/src/style.scss
  21. 0 0
      packages/@uppy/dashboard/src/utils/copyToClipboard.js
  22. 0 0
      packages/@uppy/dashboard/src/utils/copyToClipboard.test.js
  23. 1 1
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.js
  24. 17 0
      packages/@uppy/dashboard/src/utils/ignoreEvent.js
  25. 0 0
      packages/@uppy/dashboard/src/utils/truncateString.js
  26. 0 0
      packages/@uppy/dashboard/src/utils/truncateString.test.js
  27. 1 1
      packages/@uppy/drag-drop/package.json
  28. 0 14
      packages/@uppy/dropbox/src/icons.js
  29. 3 4
      packages/@uppy/dropbox/src/index.js
  30. 11 5
      packages/@uppy/google-drive/src/index.js
  31. 6 7
      packages/@uppy/informer/src/index.js
  32. 36 21
      packages/@uppy/informer/src/style.scss
  33. 5 7
      packages/@uppy/instagram/src/index.js
  34. 4 8
      packages/@uppy/provider-views/src/AuthView.js
  35. 3 3
      packages/@uppy/provider-views/src/Breadcrumbs.js
  36. 8 6
      packages/@uppy/provider-views/src/Browser.js
  37. 26 2
      packages/@uppy/provider-views/src/Item.js
  38. 144 83
      packages/@uppy/provider-views/src/style.scss
  39. 4 5
      packages/@uppy/status-bar/src/StatusBar.js
  40. 77 39
      packages/@uppy/status-bar/src/style.scss
  41. 21 24
      packages/@uppy/url/src/index.js
  42. 3 2
      packages/@uppy/url/src/style.scss
  43. 1 1
      packages/@uppy/webcam/src/CameraIcon.js
  44. 2 3
      packages/@uppy/webcam/src/PermissionsScreen.js
  45. 7 3
      packages/@uppy/webcam/src/index.js
  46. 24 15
      packages/@uppy/webcam/src/style.scss
  47. 9 1
      website/src/docs/dashboard.md
  48. 4 0
      website/src/docs/dropbox.md
  49. 4 0
      website/src/docs/google-drive.md
  50. 4 0
      website/src/docs/instagram.md
  51. 4 0
      website/src/docs/url.md
  52. 4 0
      website/src/docs/webcam.md

+ 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>
     )
   }
 }

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

@@ -0,0 +1,98 @@
+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-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-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: []

+ 272 - 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,109 @@
   }
 
 .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;
+
+  .uppy-size--md & {
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: center;
+    max-width: 600px;
+    overflow-x: initial;
+  }
 }
 
 .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 +342,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 +365,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 +374,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 +383,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 +517,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 +564,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 +595,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 +628,7 @@
   justify-content: center;
   align-items: center;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 100%;
     height: 100px;
     border: 0;
@@ -570,14 +647,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 +674,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 +720,7 @@
   left: 50%;
   transform: translate(-50%, -50%);
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 25px;
     height: 25px;
   }
@@ -662,19 +740,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 +760,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 +779,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 +789,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 +836,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 +887,7 @@
   opacity: 0.9;
   transition: all .35s ease;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 55px;
     height: 55px;
   }
@@ -834,7 +901,7 @@
     width: 18px;
     height: 18px;
 
-    .uppy-Dashboard--wide & {
+    .uppy-size--md & {
       width: 28px;
       height: 28px;
     }
@@ -845,7 +912,7 @@
     height: 18px;
     opacity: 1;
 
-    .uppy-Dashboard--wide & {
+    .uppy-size--md & {
       width: 25px;
       height: 25px;
     }
@@ -863,7 +930,7 @@
   width: 100%;
   text-shadow: 0 1px 0 rgba($color-black, 0.3);
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     display: block;
   }
 }
@@ -977,13 +1044,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 +1069,7 @@
   width: 50px;
   height: 50px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 60px;
     height: 60px;
   }
@@ -1025,7 +1093,7 @@
   line-height: 16px;
   font-size: 8px;
 
-  .uppy-Dashboard--wide & {
+  .uppy-size--md & {
     width: 18px;
     height: 18px;
     line-height: 18px;
@@ -1038,9 +1106,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 +1115,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 +1176,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%);

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

@@ -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'

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

@@ -51,6 +51,10 @@ uppy.use(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`
 
 DOM element, CSS selector, or plugin to mount the Dropbox provider into. This should normally be the Dashboard.

+ 4 - 0
website/src/docs/google-drive.md

@@ -51,6 +51,10 @@ uppy.use(GoogleDrive, {
 
 A unique identifier for this plugin. It defaults to `'GoogleDrive'`.
 
+### `title: 'Google Drive'`
+
+Title / name shown in the UI, such as Dashboard tabs. It defaults to `'Google Drive'`.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the Google Drive provider into. This should normally be the the [`@uppy/dashboard`](/docs/dashboard) plugin.

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

@@ -53,6 +53,10 @@ uppy.use(Instagram, {
 
 A unique identifier for this plugin. It defaults to `'Instagram'`.
 
+### `title: 'Instagram'`
+
+Title / name shown in the UI, such as Dashboard tabs. It defaults to `'Instagram'`.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the Instagram provider into. This should normally be the Dashboard.

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

@@ -48,6 +48,10 @@ uppy.use(Url, {
 
 A unique identifier for this plugin. Defaults to `'Url'`.
 
+### `title: 'Link'`
+
+Title / name shown in the UI, such as 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.

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

@@ -60,6 +60,10 @@ uppy.use(Webcam, {
 
 A unique identifier for this plugin. It defaults to `'Webcam'`.
 
+### `title: 'Camera'`
+
+Title / name shown in the UI, such as Dashboard tabs. It defaults to `'Camera'`.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount Webcam into.