فهرست منبع

More design improvements (#1574)

* Better focus styles

* Improve focus on the remove button

* "Add more" caption to the "+" button (todo: i18n)

* Add i18n (todo: check)

* Fix icon size

* Try removing file source (todo: remove icons)

* Simplify caption

* Revert "Try removing file source (todo: remove icons)"

This reverts commit 20c5ecb554dfab117c432ce660f0b2d423fd1d8c.

* (WIP) Use icons for Edit / Copy link actions

* Class names optimizing

* Subtle shadows

* Optimize code, align action icons on smaller screens

* Reduce Tab-btn horizontal padding on mobile

* Fix padding

* i18n

* replace arabic with persian

* initial refactor

* primarily FileItem's css - reintegrated Alex's changes

* @uppy/dashboard - unnested subcomponents from ./components

* @uppy/dashboard scss - added some spacing as per conventions

* @uppy/dashboard - changed behaviour of truncateString() to always return a predictable number of symbols

* @uppy/dashboard - subdivided FileItem.js and its css into smaller files

Removed unused css classes, made filenames avoid overflow

* @uppy/dashboard - removed unused .currentWidth prop, FileItem.js - made fileName take up at most 2 lines

* test:endtoend:prepare-ci - fix test by reordering locale lines

* @uppy/dashboard - decorative FileItem.js changes

* @uppy/dashboard - made css of FileItem.js more flat

* @uppy/Dashboard - in FileItem.js made max filename length 30 symbols

* fix image getting width: 100% in DashboardFileCard

//cc @lakesare

* FileItem.scss - removed tagnames
Alexander Zaytsev 5 سال پیش
والد
کامیت
3b4547fa2c
31فایلهای تغییر یافته به همراه891 افزوده شده و 739 حذف شده
  1. 26 0
      packages/@uppy/core/src/_utils.scss
  2. 2 0
      packages/@uppy/core/src/_variables.scss
  3. 0 208
      packages/@uppy/dashboard/src/components/FileItem.js
  4. 65 0
      packages/@uppy/dashboard/src/components/FileItem/Buttons/index.js
  5. 59 0
      packages/@uppy/dashboard/src/components/FileItem/Buttons/index.scss
  6. 60 0
      packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.js
  7. 48 0
      packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.scss
  8. 25 0
      packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.js
  9. 51 0
      packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.scss
  10. 4 3
      packages/@uppy/dashboard/src/components/FileItem/FileProgress/PauseResumeCancelIcon.js
  11. 83 0
      packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.js
  12. 159 0
      packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.scss
  13. 75 0
      packages/@uppy/dashboard/src/components/FileItem/index.js
  14. 103 0
      packages/@uppy/dashboard/src/components/FileItem/index.scss
  15. 1 2
      packages/@uppy/dashboard/src/components/FileList.js
  16. 1 0
      packages/@uppy/dashboard/src/components/PickerPanelTopBar.js
  17. 23 1
      packages/@uppy/dashboard/src/components/icons.js
  18. 1 2
      packages/@uppy/dashboard/src/index.js
  19. 44 504
      packages/@uppy/dashboard/src/style.scss
  20. 24 7
      packages/@uppy/dashboard/src/utils/truncateString.js
  21. 26 11
      packages/@uppy/dashboard/src/utils/truncateString.test.js
  22. 1 0
      packages/@uppy/locales/src/de_DE.js
  23. 1 0
      packages/@uppy/locales/src/en_US.js
  24. 1 0
      packages/@uppy/locales/src/es_ES.js
  25. 1 0
      packages/@uppy/locales/src/fa_IR.js
  26. 1 0
      packages/@uppy/locales/src/fr_FR.js
  27. 1 0
      packages/@uppy/locales/src/it_IT.js
  28. 1 0
      packages/@uppy/locales/src/nl_NL.js
  29. 2 1
      packages/@uppy/locales/src/ru_RU.js
  30. 1 0
      packages/@uppy/locales/src/zh_CN.js
  31. 1 0
      packages/@uppy/locales/src/zh_TW.js

+ 26 - 0
packages/@uppy/core/src/_utils.scss

@@ -23,3 +23,29 @@
   border: 0;
   border: 0;
   color: inherit;
   color: inherit;
 }
 }
+
+@mixin highlight-focus() {
+  &:hover {
+    color: darken($blue, 10%);
+  }
+
+  &:focus {
+    outline: none;
+    background-color: $highlight;
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+}
+
+@mixin blue-border-focus() {
+  &:focus {
+    outline: none;
+    box-shadow: 0 0 0 3px rgba($blue, 0.5);
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+}

+ 2 - 0
packages/@uppy/core/src/_variables.scss

@@ -25,6 +25,8 @@ $gray-700: #525252 !default;
 $gray-800: #333 !default;
 $gray-800: #333 !default;
 $gray-900: #1f1f1f !default;
 $gray-900: #1f1f1f !default;
 
 
+$highlight: #eceef2;
+
 $uppy-pink: #eb2177;
 $uppy-pink: #eb2177;
 
 
 // Sizes
 // Sizes

+ 0 - 208
packages/@uppy/dashboard/src/components/FileItem.js

@@ -1,208 +0,0 @@
-const getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension')
-const truncateString = require('../utils/truncateString')
-const copyToClipboard = require('../utils/copyToClipboard')
-const prettyBytes = require('prettier-bytes')
-const FileItemProgress = require('./FileItemProgress')
-const getFileTypeIcon = require('../utils/getFileTypeIcon')
-const FilePreview = require('./FilePreview')
-const { iconRetry } = require('./icons')
-const classNames = require('classnames')
-const { h } = require('preact')
-
-function FileItemProgressWrapper (props) {
-  if (props.hideRetryButton && props.error) {
-    return
-  }
-
-  if (props.isUploaded ||
-      (props.hidePauseResumeCancelButtons && !props.error)) {
-    return <div class="uppy-DashboardItem-progressIndicator">
-      <FileItemProgress
-        progress={props.file.progress.percentage}
-        fileID={props.file.id}
-        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
-        individualCancellation={props.individualCancellation}
-      />
-    </div>
-  }
-
-  return <button
-    class="uppy-DashboardItem-progressIndicator"
-    type="button"
-    aria-label={props.progressIndicatorTitle}
-    title={props.progressIndicatorTitle}
-    onclick={props.onPauseResumeCancelRetry}>
-    {props.error
-      ? props.hideRetryButton ? null : iconRetry()
-      : <FileItemProgress
-        progress={props.file.progress.percentage}
-        fileID={props.file.id}
-        individualCancellation={props.individualCancellation}
-        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
-      />
-    }
-  </button>
-}
-
-module.exports = function FileItem (props) {
-  const file = props.file
-  const acquirers = props.acquirers
-
-  const isProcessing = file.progress.preprocess || file.progress.postprocess
-  const isUploaded = file.progress.uploadComplete && !isProcessing && !file.error
-  const uploadInProgressOrComplete = file.progress.uploadStarted || isProcessing
-  const uploadInProgress = (file.progress.uploadStarted && !file.progress.uploadComplete) || isProcessing
-  const isPaused = file.isPaused || false
-  const error = file.error || false
-
-  const fileName = getFileNameAndExtension(file.meta.name).name
-  const truncatedFileName = props.isWide ? truncateString(fileName, 30) : fileName
-
-  function onPauseResumeCancelRetry (ev) {
-    if (isUploaded) return
-
-    if (error && !props.hideRetryButton) {
-      props.retryUpload(file.id)
-      return
-    }
-
-    if (props.hidePauseResumeCancelButtons) {
-      return
-    }
-
-    if (props.resumableUploads) {
-      props.pauseUpload(file.id)
-    } else if (props.individualCancellation) {
-      props.cancelUpload(file.id)
-    }
-  }
-
-  function progressIndicatorTitle (props) {
-    if (isUploaded) {
-      return props.i18n('uploadComplete')
-    }
-
-    if (error) {
-      return props.i18n('retryUpload')
-    }
-
-    if (props.resumableUploads) {
-      if (file.isPaused) {
-        return props.i18n('resumeUpload')
-      }
-      return props.i18n('pauseUpload')
-    } else if (props.individualCancellation) {
-      return props.i18n('cancelUpload')
-    }
-
-    return ''
-  }
-
-  const dashboardItemClass = classNames(
-    'uppy-DashboardItem',
-    { 'is-inprogress': uploadInProgress },
-    { 'is-processing': isProcessing },
-    { 'is-complete': isUploaded },
-    { 'is-paused': isPaused },
-    { 'is-error': error },
-    { 'is-resumable': props.resumableUploads },
-    { 'is-noIndividualCancellation': !props.individualCancellation }
-  )
-
-  const showRemoveButton = props.individualCancellation
-    ? !isUploaded
-    : !uploadInProgress && !isUploaded
-
-  return <li class={dashboardItemClass} id={`uppy_${file.id}`}>
-    <div class="uppy-DashboardItem-preview">
-      <div class="uppy-DashboardItem-previewInnerWrap" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
-        {props.showLinkToFileUploadResult && file.uploadURL
-          ? <a class="uppy-DashboardItem-previewLink" href={file.uploadURL} rel="noreferrer noopener" target="_blank" aria-label={file.meta.name} />
-          : null
-        }
-        <FilePreview file={file} />
-      </div>
-      <div class="uppy-DashboardItem-progress">
-        <FileItemProgressWrapper
-          progressIndicatorTitle={progressIndicatorTitle(props)}
-          onPauseResumeCancelRetry={onPauseResumeCancelRetry}
-          file={file}
-          error={error}
-          isUploaded={isUploaded}
-          {...props} />
-      </div>
-    </div>
-    <div class="uppy-DashboardItem-info">
-      <div class="uppy-DashboardItem-name">
-        {file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName}
-      </div>
-      <div class="uppy-DashboardItem-status">
-        {
-          file.data.size && [
-            <div class="uppy-DashboardItem-statusSize">
-              {prettyBytes(file.data.size)}
-            </div>,
-            <span class="uppy-DashboardItem-statusbar-dot">·</span>
-          ]
-        }
-
-        {
-          (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 })}>
-                    {acquirer.icon()}
-                  </span>
-                }
-              })}
-            </div>,
-            <span class="uppy-DashboardItem-statusbar-dot">·</span>
-          ]
-        }
-        {
-          (!uploadInProgressOrComplete && props.metaFields && props.metaFields.length) && [
-            <button class="uppy-u-reset uppy-DashboardItem-edit"
-              type="button"
-              aria-label={props.i18n('editFile') + ' ' + fileName}
-              title={props.i18n('editFile')}
-              onclick={(e) => props.toggleFileCard(file.id)}>
-              {props.i18n('edit')}
-            </button>,
-            <span class="uppy-DashboardItem-statusbar-dot">·</span>
-          ]
-        }
-
-        {props.showLinkToFileUploadResult && file.uploadURL
-          ? <button class="uppy-u-reset 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)
-            }}>{props.i18n('link')}</button>
-          : ''
-        }
-      </div>
-    </div>
-    <div class="uppy-DashboardItem-action">
-      {showRemoveButton &&
-        <button class="uppy-DashboardItem-remove"
-          type="button"
-          aria-label={props.i18n('removeFile')}
-          title={props.i18n('removeFile')}
-          onclick={() => props.removeFile(file.id)}>
-          <svg aria-hidden="true" focusable="false" class="UppyIcon" width="18" height="18" viewBox="0 0 18 18">
-            <path d="M9 0C4.034 0 0 4.034 0 9s4.034 9 9 9 9-4.034 9-9-4.034-9-9-9z" />
-            <path fill="#FFF" d="M13 12.222l-.778.778L9 9.778 5.778 13 5 12.222 8.222 9 5 5.778 5.778 5 9 8.222 12.222 5l.778.778L9.778 9z" />
-          </svg>
-        </button>
-      }
-    </div>
-  </li>
-}

+ 65 - 0
packages/@uppy/dashboard/src/components/FileItem/Buttons/index.js

@@ -0,0 +1,65 @@
+const { h } = require('preact')
+const copyToClipboard = require('../../../utils/copyToClipboard')
+
+const { iconPencil, iconCross, iconCopyLink } = require('../../icons')
+
+const renderEditButton = (props) => (
+  !props.uploadInProgressOrComplete &&
+  props.metaFields &&
+  props.metaFields.length &&
+  <button
+    class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--edit"
+    type="button"
+    aria-label={props.i18n('editFile') + ' ' + props.file.meta.name}
+    title={props.i18n('editFile')}
+    onclick={(e) => props.toggleFileCard(props.file.id)}
+  >
+    {iconPencil()}
+  </button>
+)
+
+const renderRemoveButton = (props) => (
+  props.showRemoveButton &&
+  <button
+    class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--remove"
+    type="button"
+    aria-label={props.i18n('removeFile')}
+    title={props.i18n('removeFile')}
+    onclick={() => props.removeFile(props.file.id)}
+  >
+    {iconCross()}
+  </button>
+)
+
+const copyLinkToClipboard = (event, props) =>
+  copyToClipboard(props.file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
+    .then(() => {
+      props.log('Link copied to clipboard.')
+      props.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
+    })
+    .catch(props.log)
+    // avoid losing focus
+    .then(() => event.target.focus({ preventScroll: true }))
+
+const renderCopyLinkButton = (props) => (
+  props.showLinkToFileUploadResult &&
+  props.file.uploadURL &&
+  <button class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--copyLink"
+    type="button"
+    aria-label={props.i18n('copyLink')}
+    title={props.i18n('copyLink')}
+    onclick={(event) => copyLinkToClipboard(event, props)}
+  >
+    {iconCopyLink()}
+  </button>
+)
+
+module.exports = function Buttons (props) {
+  return (
+    <div className="uppy-DashboardItem-actionWrapper">
+      {renderEditButton(props)}
+      {renderCopyLinkButton(props)}
+      {renderRemoveButton(props)}
+    </div>
+  )
+}

+ 59 - 0
packages/@uppy/dashboard/src/components/FileItem/Buttons/index.scss

@@ -0,0 +1,59 @@
+// On both mobile and .md+ screens
+.uppy-DashboardItem-action {
+  @include blue-border-focus;
+  cursor: pointer;
+  color: $gray-500;
+  &:hover {
+    opacity: 1;
+    color: $gray-900;
+  }
+}
+.uppy-DashboardItem-action--remove {
+  color: $gray-900;
+  opacity: 0.95;
+}
+
+// Only for mobile screens
+.uppy-Dashboard:not(.uppy-size--md) {
+  // Vertically center Edit&Remove buttons on mobile
+  .uppy-DashboardItem-actionWrapper {
+    display: flex;
+    align-items: center;
+  }
+  // Same inline design for Edit, Remove, and CopyLink buttons
+  .uppy-DashboardItem-action {
+    width: 22px;
+    height: 22px;
+    padding: 3px;
+    margin-left: 3px;
+    &:focus{
+      border-radius: 3px;
+    }
+  }
+}
+// Only for screens bigger than .md
+.uppy-size--md {
+  // Edit and CopyLink buttons are inline
+  .uppy-DashboardItem-action--copyLink,
+  .uppy-DashboardItem-action--edit {
+    width: 16px;
+    height: 16px;
+    padding: 0;
+    &:focus {
+      border-radius: 3px;
+    }
+  }
+  // Remove button is in the top right corner
+  .uppy-DashboardItem-action--remove {
+    z-index: $zIndex-3;
+    position: absolute;
+    top: -8px;
+    right: -8px;
+    width: 18px;
+    height: 18px;
+    padding: 0;
+    &:focus {
+      border-radius: 50%;
+    }
+  }
+}

+ 60 - 0
packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.js

@@ -0,0 +1,60 @@
+const { h } = require('preact')
+const prettyBytes = require('prettier-bytes')
+const truncateString = require('../../../utils/truncateString')
+
+const renderAcquirerIcon = (acquirer, props) =>
+  <span title={props.i18n('fileSource', { name: acquirer.name })}>
+    {acquirer.icon()}
+  </span>
+
+const renderFileSource = (props) => (
+  props.file.source &&
+  props.file.source !== props.id &&
+  <div class="uppy-DashboardItem-sourceIcon">
+    {props.acquirers.map(acquirer => {
+      if (acquirer.id === props.file.source) {
+        return renderAcquirerIcon(acquirer, props)
+      }
+    })}
+  </div>
+)
+
+const renderFileName = (props) => {
+  // Take up at most 2 lines on any screen
+  let maxNameLength
+  // For very small mobile screens
+  if (props.containerWidth <= 352) {
+    maxNameLength = 35
+  // For regular mobile screens
+  } else if (props.containerWidth <= 576) {
+    maxNameLength = 60
+  // For desktops
+  } else {
+    maxNameLength = 30
+  }
+
+  return (
+    <div class="uppy-DashboardItem-name" title={props.file.meta.name}>
+      {truncateString(props.file.meta.name, maxNameLength)}
+    </div>
+  )
+}
+
+const renderFileSize = (props) => (
+  props.file.data.size &&
+  <div class="uppy-DashboardItem-statusSize">
+    {prettyBytes(props.file.data.size)}
+  </div>
+)
+
+module.exports = function FileInfo (props) {
+  return (
+    <div class="uppy-DashboardItem-fileInfo">
+      {renderFileName(props)}
+      <div class="uppy-DashboardItem-status">
+        {renderFileSize(props)}
+        {renderFileSource(props)}
+      </div>
+    </div>
+  )
+}

+ 48 - 0
packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.scss

@@ -0,0 +1,48 @@
+.uppy-DashboardItem-fileInfo {
+  padding-right: 5px;
+}
+  .uppy-DashboardItem-name {
+    font-size: 12px;
+    line-height: 1.3;
+    font-weight: 500;
+    margin-bottom: 4px;
+
+    word-break: break-all;
+    // Must be present
+    // [Practical check] Firefox, '384048411015659...2309520384n.png' filename
+    word-wrap: anywhere;
+  }
+
+  .uppy-DashboardItem-status {
+    font-size: 11px;
+    line-height: 1.3;
+    font-weight: normal;
+    color: $gray-600;
+  }
+    .uppy-DashboardItem-statusSize {
+      display: inline-block;
+      vertical-align: bottom;
+      text-transform: uppercase;
+    }
+    .uppy-DashboardItem-sourceIcon {
+      display: inline-block;
+      vertical-align: bottom;
+      color: $gray-400;
+      &:not(:first-child) {
+        position: relative;
+        margin-left: 14px;
+      }
+      svg,
+      svg * {
+        max-width: 100%;
+        max-height: 100%;
+        display: inline-block;
+        vertical-align: text-bottom;
+        overflow: hidden;
+        fill: currentColor;
+        width: 11px;
+        height: 12px;
+      }
+    }
+  // ...uppy-DashboardItem-status|
+// ...uppy-DashboardItem-fileInfo|

+ 25 - 0
packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.js

@@ -0,0 +1,25 @@
+const { h } = require('preact')
+const FilePreview = require('../../FilePreview')
+const getFileTypeIcon = require('../../../utils/getFileTypeIcon')
+
+module.exports = function FilePreviewAndLink (props) {
+  return (
+    <div
+      class="uppy-DashboardItem-previewInnerWrap"
+      style={{ backgroundColor: getFileTypeIcon(props.file.type).color }}
+    >
+      {
+        props.showLinkToFileUploadResult &&
+        props.file.uploadURL &&
+        <a
+          class="uppy-DashboardItem-previewLink"
+          href={props.file.uploadURL}
+          rel="noreferrer noopener"
+          target="_blank"
+          aria-label={props.file.meta.name}
+        />
+      }
+      <FilePreview file={props.file} />
+    </div>
+  )
+}

+ 51 - 0
packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.scss

@@ -0,0 +1,51 @@
+.uppy-DashboardItem-previewInnerWrap {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  // For :after positioning
+  position: relative;
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+
+  box-shadow: 0 0 2px 0 rgba($black, 0.4);
+  border-radius: 3px;
+
+  .uppy-size--md & {
+    box-shadow: 0 1px 2px rgba($black, 0.15);
+  }
+}
+  .uppy-DashboardItem-previewInnerWrap:after {
+    content: '';
+    position: absolute;
+    left: 0; right: 0;
+    top: 0; bottom: 0;
+    background-color: rgba($black, 0.65) /* no !important */;
+    display: none;
+    z-index: $zIndex-2;
+  }
+
+  .uppy-DashboardItem-previewLink {
+    position: absolute;
+    left: 0; right: 0;
+    top: 0; bottom: 0;
+
+    z-index: $zIndex-3;
+    &:focus{
+      box-shadow: inset 0 0 0 3px lighten($blue, 20%);
+    }
+  }
+
+  .uppy-DashboardItem-preview img.uppy-DashboardItem-previewImg {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    // Fixes file previews being partially invisible in safari (for some pics only).
+    // (https://stackoverflow.com/a/27971913/3192470)
+    transform: translateZ(0);
+    // We need a repeated border-radius because of the transform.
+    border-radius: 3px;
+  }
+// ...uppy-DashboardItem-previewInnerWrap|

+ 4 - 3
packages/@uppy/dashboard/src/components/FileItemProgress.js → packages/@uppy/dashboard/src/components/FileItem/FileProgress/PauseResumeCancelIcon.js

@@ -9,7 +9,7 @@ const circleLength = 2 * Math.PI * 15
 
 
 // stroke-dashoffset is a percentage of the progress from circleLength,
 // stroke-dashoffset is a percentage of the progress from circleLength,
 // substracted from circleLength, because its an offset
 // substracted from circleLength, because its an offset
-module.exports = (props) => {
+module.exports = function PauseResumeCancelIcon (props) {
   return (
   return (
     <svg aria-hidden="true" focusable="false" width="70" height="70" viewBox="0 0 36 36" class="UppyIcon UppyIcon-progressCircle">
     <svg aria-hidden="true" focusable="false" width="70" height="70" viewBox="0 0 36 36" class="UppyIcon UppyIcon-progressCircle">
       <g class="progress-group">
       <g class="progress-group">
@@ -19,7 +19,8 @@ module.exports = (props) => {
           stroke-dashoffset={circleLength - (circleLength / 100 * props.progress)}
           stroke-dashoffset={circleLength - (circleLength / 100 * props.progress)}
         />
         />
       </g>
       </g>
-      {!props.hidePauseResumeCancelButtons ? (
+      {
+        !props.hidePauseResumeCancelButtons &&
         <g>
         <g>
           <polygon class="play" transform="translate(3, 3)" points="12 20 12 10 20 15" />
           <polygon class="play" transform="translate(3, 3)" points="12 20 12 10 20 15" />
           <g class="pause" transform="translate(14.5, 13)">
           <g class="pause" transform="translate(14.5, 13)">
@@ -28,7 +29,7 @@ module.exports = (props) => {
           </g>
           </g>
           <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
           <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
         </g>
         </g>
-      ) : null}
+      }
       <polygon class="check" transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" />
       <polygon class="check" transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" />
     </svg>
     </svg>
   )
   )

+ 83 - 0
packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.js

@@ -0,0 +1,83 @@
+const { h } = require('preact')
+const { iconRetry } = require('../../icons')
+const PauseResumeCancelIcon = require('./PauseResumeCancelIcon')
+
+function onPauseResumeCancelRetry (props) {
+  if (props.isUploaded) return
+
+  if (props.error && !props.hideRetryButton) {
+    props.retryUpload(props.file.id)
+    return
+  }
+
+  if (props.hidePauseResumeCancelButtons) {
+    return
+  }
+
+  if (props.resumableUploads) {
+    props.pauseUpload(props.file.id)
+  } else if (props.individualCancellation) {
+    props.cancelUpload(props.file.id)
+  }
+}
+
+function progressIndicatorTitle (props) {
+  if (props.isUploaded) {
+    return props.i18n('uploadComplete')
+  }
+
+  if (props.error) {
+    return props.i18n('retryUpload')
+  }
+
+  if (props.resumableUploads) {
+    if (props.file.isPaused) {
+      return props.i18n('resumeUpload')
+    }
+    return props.i18n('pauseUpload')
+  } else if (props.individualCancellation) {
+    return props.i18n('cancelUpload')
+  }
+
+  return ''
+}
+
+module.exports = function FileProgress (props) {
+  if (props.hideRetryButton && props.error) {
+    return <div class="uppy-DashboardItem-progress" />
+  } else if (
+    props.isUploaded ||
+    (props.hidePauseResumeCancelButtons && !props.error)
+  ) {
+    return (
+      <div class="uppy-DashboardItem-progress">
+        <div class="uppy-DashboardItem-progressIndicator">
+          <PauseResumeCancelIcon
+            progress={props.file.progress.percentage}
+            hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
+          />
+        </div>
+      </div>
+    )
+  } else {
+    return (
+      <div class="uppy-DashboardItem-progress">
+        <button
+          class="uppy-u-reset uppy-DashboardItem-progressIndicator"
+          type="button"
+          aria-label={progressIndicatorTitle(props)}
+          title={progressIndicatorTitle(props)}
+          onclick={() => onPauseResumeCancelRetry(props)}
+        >
+          {props.error
+            ? props.hideRetryButton ? null : iconRetry()
+            : <PauseResumeCancelIcon
+              progress={props.file.progress.percentage}
+              hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
+            />
+          }
+        </button>
+      </div>
+    )
+  }
+}

+ 159 - 0
packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.scss

@@ -0,0 +1,159 @@
+.uppy-DashboardItem-progress {
+  display: none;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: $zIndex-3;
+  color: $white;
+  text-align: center;
+  width: 120px;
+  transition: all .35 ease;
+}
+  .uppy-DashboardItem-progressIndicator {
+    display: inline-block;
+    width: 38px;
+    height: 38px;
+    opacity: 0.9;
+    cursor: pointer;
+    &:focus{
+      outline: none;
+      svg.UppyIcon-progressCircle .bg,
+      svg.retry{
+        fill: lighten($blue, 20%);
+      }
+    }
+  }
+// ...uppy-DashboardItem-progress|
+
+svg.UppyIcon-progressCircle {
+  width: 100%;
+  height: 100%;
+  .bg {
+    stroke: rgba($white, 0.4);
+    opacity: 0;
+  }
+  .progress {
+    stroke: $white;
+    transition: stroke-dashoffset .5s ease-out;
+    opacity: 0;
+  }
+  .play {
+    stroke: $white;
+    fill: $white;
+    opacity: 0;
+    transition: all 0.2s;
+    display: none;
+  }
+  .cancel {
+    fill: $white;
+    opacity: 0;
+    transition: all 0.2s;
+  }
+  .pause {
+    stroke: $white;
+    fill: $white;
+    opacity: 0;
+    transition: all 0.2s;
+    display: none;
+  }
+  .check {
+    opacity: 0;
+    fill: $white;
+    transition: all 0.2s;
+  }
+}
+svg.UppyIcon.retry {
+  fill: $white;
+}
+
+
+// Svg styles that depend on the state of the file.
+.uppy-DashboardItem.is-complete .uppy-DashboardItem-progress {
+  transform: initial;
+  top: -9px;
+  right: -8px;
+  left: initial;
+  width: auto;
+}
+.uppy-DashboardItem.is-inprogress .uppy-DashboardItem-progress,
+.uppy-DashboardItem.is-complete .uppy-DashboardItem-progress,
+.uppy-DashboardItem.is-error .uppy-DashboardItem-progress {
+  display: block;
+}
+
+.uppy-DashboardItem.is-error .uppy-DashboardItem-progressIndicator {
+  width: 18px;
+  height: 18px;
+
+  .uppy-size--md & {
+    width: 28px;
+    height: 28px;
+  }
+}
+
+.uppy-DashboardItem.is-complete .uppy-DashboardItem-progressIndicator {
+  width: 18px;
+  height: 18px;
+  opacity: 1;
+
+  .uppy-size--md & {
+    width: 22px;
+    height: 22px;
+  }
+}
+
+.uppy-DashboardItem.is-paused svg.UppyIcon-progressCircle {
+  .pause {
+    opacity: 0;
+  }
+  .play {
+    opacity: 1;
+  }
+}
+
+.uppy-DashboardItem.is-noIndividualCancellation {
+  .uppy-DashboardItem-progressIndicator {
+    cursor: default;
+  }
+
+  .cancel {
+    display: none;
+  }
+}
+
+.uppy-DashboardItem.is-processing .uppy-DashboardItem-progress {
+  opacity: 0;
+}
+
+.uppy-DashboardItem.is-complete {
+  .uppy-DashboardItem-progressIndicator {
+    cursor: default;
+  }
+
+  .progress {
+    stroke: $green;
+    fill: $green;
+    opacity: 1;
+  }
+
+  .check {
+    opacity: 1;
+  }
+}
+
+.uppy-size--md .uppy-DashboardItem-progressIndicator {
+  width: 55px;
+  height: 55px;
+}
+
+.uppy-DashboardItem.is-resumable {
+  .pause, .play { display: block; }
+  .cancel { display: none; }
+}
+
+.uppy-DashboardItem.is-inprogress {
+  .bg, .progress, .pause, .cancel {
+    opacity: 1;
+  }
+}

+ 75 - 0
packages/@uppy/dashboard/src/components/FileItem/index.js

@@ -0,0 +1,75 @@
+const { h } = require('preact')
+const classNames = require('classnames')
+
+const FilePreviewAndLink = require('./FilePreviewAndLink')
+const FileProgress = require('./FileProgress')
+const FileInfo = require('./FileInfo')
+const Buttons = require('./Buttons')
+
+module.exports = function FileItem (props) {
+  const file = props.file
+
+  const isProcessing = file.progress.preprocess || file.progress.postprocess
+  const isUploaded = file.progress.uploadComplete && !isProcessing && !file.error
+  const uploadInProgressOrComplete = file.progress.uploadStarted || isProcessing
+  const uploadInProgress = (file.progress.uploadStarted && !file.progress.uploadComplete) || isProcessing
+  const isPaused = file.isPaused || false
+  const error = file.error || false
+
+  const showRemoveButton = props.individualCancellation
+    ? !isUploaded
+    : !uploadInProgress && !isUploaded
+
+  const dashboardItemClass = classNames(
+    'uppy-u-reset',
+    'uppy-DashboardItem',
+    { 'is-inprogress': uploadInProgress },
+    { 'is-processing': isProcessing },
+    { 'is-complete': isUploaded },
+    { 'is-paused': isPaused },
+    { 'is-error': error },
+    { 'is-resumable': props.resumableUploads },
+    { 'is-noIndividualCancellation': !props.individualCancellation }
+  )
+
+  return (
+    <li class={dashboardItemClass} id={`uppy_${file.id}`}>
+      <div class="uppy-DashboardItem-preview">
+        <FilePreviewAndLink
+          file={file}
+          showLinkToFileUploadResult={props.showLinkToFileUploadResult}
+        />
+        <FileProgress
+          error={error}
+          isUploaded={isUploaded}
+          {...props}
+        />
+      </div>
+
+      <div class="uppy-DashboardItem-fileInfoAndButtons">
+        <FileInfo
+          file={file}
+          id={props.id}
+          acquirers={props.acquirers}
+          containerWidth={props.containerWidth}
+          i18n={props.i18n}
+        />
+        <Buttons
+          file={file}
+          metaFields={props.metaFields}
+
+          showLinkToFileUploadResult={props.showLinkToFileUploadResult}
+          showRemoveButton={showRemoveButton}
+
+          uploadInProgressOrComplete={uploadInProgressOrComplete}
+          removeFile={props.removeFile}
+          toggleFileCard={props.toggleFileCard}
+
+          i18n={props.i18n}
+          log={props.log}
+          info={props.info}
+        />
+      </div>
+    </li>
+  )
+}

+ 103 - 0
packages/@uppy/dashboard/src/components/FileItem/index.scss

@@ -0,0 +1,103 @@
+@import './FilePreviewAndLink/index.scss';
+@import './FileProgress/index.scss';
+@import './FileInfo/index.scss';
+@import './Buttons/index.scss';
+
+.uppy-DashboardItem {
+  // @media only mobile
+  .uppy-Dashboard:not(.uppy-size--md) & {
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid $gray-200;
+    padding: 10px;
+    padding-right: 0;
+  }
+  // @media bigger than .md
+  $rl-margin: 15px;
+  .uppy-size--md & {
+    // For the Remove button
+    position: relative;
+
+    display: block;
+    float: left;
+    margin: 5px $rl-margin;
+    width: calc(33.333% - #{$rl-margin} - #{$rl-margin});
+    height: 215px;
+  }
+  .uppy-size--lg & {
+    margin: 5px $rl-margin;
+    width: calc(25% - #{$rl-margin} - #{$rl-margin});
+    height: 190px;
+  }
+  .uppy-size--xl & {
+    width: calc(20% - #{$rl-margin} - #{$rl-margin});
+    height: 210px;
+  }
+}
+  .uppy-DashboardItem-preview {
+    // for the FileProgress.js icons
+    position: relative;
+
+    // @media only mobile
+    .uppy-Dashboard:not(.uppy-size--md) & {
+      flex-shrink: 0;
+      flex-grow: 0;
+      width: 50px;
+      height: 50px;
+    }
+    // @media bigger than .md
+    .uppy-size--md & {
+      width: 100%;
+      height: 140px;
+    }
+    .uppy-size--lg & {
+      height: 120px;
+    }
+    .uppy-size--xl & {
+      height: 140px;
+    }
+  }
+  .uppy-DashboardItem-fileInfoAndButtons {
+    flex-grow: 1;
+    padding-right: 8px;
+    padding-left: 12px;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .uppy-size--md & {
+      align-items: flex-start;
+      width: 100%;
+      padding: 0;
+      padding-top: 9px;
+    }
+  }
+    .uppy-DashboardItem-fileInfo {
+      flex-grow: 1;
+      flex-shrink: 1;
+    }
+    .uppy-DashboardItem-actionWrapper {
+      flex-grow: 0;
+      flex-shrink: 0;
+    }
+  // ...uppy-DashboardItem-fileInfoAndButtons|
+// ...uppy-DashboardItem|
+
+
+// Css that depends on status of the file (could be logic in js instead?)
+.uppy-DashboardItem.is-inprogress {
+  .uppy-DashboardItem-previewInnerWrap:after {
+    display: block;
+  }
+}
+.uppy-DashboardItem.is-error {
+  .uppy-DashboardItem-previewInnerWrap:after {
+    display: block;
+  }
+}
+.uppy-DashboardItem.is-inprogress:not(.is-resumable) {
+  .uppy-DashboardItem-action--remove {
+    display: none;
+  }
+}

+ 1 - 2
packages/@uppy/dashboard/src/components/FileList.js

@@ -1,4 +1,4 @@
-const FileItem = require('./FileItem')
+const FileItem = require('./FileItem/index.js')
 const classNames = require('classnames')
 const classNames = require('classnames')
 const { h } = require('preact')
 const { h } = require('preact')
 
 
@@ -17,7 +17,6 @@ module.exports = (props) => {
       {Object.keys(props.files).map((fileID) => (
       {Object.keys(props.files).map((fileID) => (
         <FileItem
         <FileItem
           {...props}
           {...props}
-          acquirers={props.acquirers}
           file={props.files[fileID]}
           file={props.files[fileID]}
         />
         />
       ))}
       ))}

+ 1 - 0
packages/@uppy/dashboard/src/components/PickerPanelTopBar.js

@@ -97,6 +97,7 @@ function PanelTopBar (props) {
           <svg aria-hidden="true" focusable="false" class="UppyIcon" width="15" height="15" viewBox="0 0 15 15">
           <svg aria-hidden="true" focusable="false" class="UppyIcon" width="15" height="15" viewBox="0 0 15 15">
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
           </svg>
           </svg>
+          <span class="uppy-DashboardContent-addMoreCaption">{props.i18n('addMore')}</span>
         </button>
         </button>
       }
       }
     </div>
     </div>

+ 23 - 1
packages/@uppy/dashboard/src/components/icons.js

@@ -85,6 +85,25 @@ function iconText () {
   </svg>
   </svg>
 }
 }
 
 
+function iconCopyLink () {
+  return <svg aria-hidden="true" focusable="false" class="UppyIcon" width="14" height="14" viewBox="0 0 14 12">
+    <path d="M7.94 7.703a2.613 2.613 0 0 1-.626 2.681l-.852.851a2.597 2.597 0 0 1-1.849.766A2.616 2.616 0 0 1 2.764 7.54l.852-.852a2.596 2.596 0 0 1 2.69-.625L5.267 7.099a1.44 1.44 0 0 0-.833.407l-.852.851a1.458 1.458 0 0 0 1.03 2.486c.39 0 .755-.152 1.03-.426l.852-.852c.231-.231.363-.522.406-.824l1.04-1.038zm4.295-5.937A2.596 2.596 0 0 0 10.387 1c-.698 0-1.355.272-1.849.766l-.852.851a2.614 2.614 0 0 0-.624 2.688l1.036-1.036c.041-.304.173-.6.407-.833l.852-.852c.275-.275.64-.426 1.03-.426a1.458 1.458 0 0 1 1.03 2.486l-.852.851a1.442 1.442 0 0 1-.824.406l-1.04 1.04a2.596 2.596 0 0 0 2.683-.628l.851-.85a2.616 2.616 0 0 0 0-3.697zm-6.88 6.883a.577.577 0 0 0 .82 0l3.474-3.474a.579.579 0 1 0-.819-.82L5.355 7.83a.579.579 0 0 0 0 .819z" />
+  </svg>
+}
+
+function iconPencil () {
+  return <svg aria-hidden="true" focusable="false" class="UppyIcon" width="14" height="14" viewBox="0 0 14 14">
+    <g fill-rule="evenodd"><path d="M1.5 10.793h2.793A1 1 0 0 0 5 10.5L11.5 4a1 1 0 0 0 0-1.414L9.707.793a1 1 0 0 0-1.414 0l-6.5 6.5A1 1 0 0 0 1.5 8v2.793zm1-1V8L9 1.5l1.793 1.793-6.5 6.5H2.5z" fill-rule="nonzero" /><rect x="1" y="12.293" width="11" height="1" rx=".5" /><path fill-rule="nonzero" d="M6.793 2.5L9.5 5.207l.707-.707L7.5 1.793z" /></g>
+  </svg>
+}
+
+function iconCross () {
+  return <svg aria-hidden="true" focusable="false" class="UppyIcon" width="18" height="18" viewBox="0 0 18 18">
+    <path d="M9 0C4.034 0 0 4.034 0 9s4.034 9 9 9 9-4.034 9-9-4.034-9-9-9z" />
+    <path fill="#FFF" d="M13 12.222l-.778.778L9 9.778 5.778 13 5 12.222 8.222 9 5 5.778 5.778 5 9 8.222 12.222 5l.778.778L9.778 9z" />
+  </svg>
+}
+
 module.exports = {
 module.exports = {
   defaultPickerIcon,
   defaultPickerIcon,
   iconCopy,
   iconCopy,
@@ -97,5 +116,8 @@ module.exports = {
   iconVideo,
   iconVideo,
   iconPDF,
   iconPDF,
   iconFile,
   iconFile,
-  iconText
+  iconText,
+  iconCopyLink,
+  iconPencil,
+  iconCross
 }
 }

+ 1 - 2
packages/@uppy/dashboard/src/index.js

@@ -53,6 +53,7 @@ module.exports = class Dashboard extends Plugin {
         fileSource: 'File source: %{name}',
         fileSource: 'File source: %{name}',
         done: 'Done',
         done: 'Done',
         back: 'Back',
         back: 'Back',
+        addMore: 'Add more',
         removeFile: 'Remove file',
         removeFile: 'Remove file',
         editFile: 'Edit file',
         editFile: 'Edit file',
         editing: 'Editing %{file}',
         editing: 'Editing %{file}',
@@ -805,8 +806,6 @@ module.exports = class Dashboard extends Plugin {
       height: this.opts.height,
       height: this.opts.height,
       showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,
       showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,
       proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
       proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
-      currentWidth: pluginState.containerWidth,
-      isWide: pluginState.containerWidth > 400,
       containerWidth: pluginState.containerWidth,
       containerWidth: pluginState.containerWidth,
       areInsidesReadyToBeVisible: pluginState.areInsidesReadyToBeVisible,
       areInsidesReadyToBeVisible: pluginState.areInsidesReadyToBeVisible,
       isTargetDOMEl: this.isTargetDOMEl,
       isTargetDOMEl: this.isTargetDOMEl,

+ 44 - 504
packages/@uppy/dashboard/src/style.scss

@@ -4,6 +4,9 @@
 @import '@uppy/status-bar/src/style.scss';
 @import '@uppy/status-bar/src/style.scss';
 @import '@uppy/provider-views/src/style.scss';
 @import '@uppy/provider-views/src/style.scss';
 
 
+// Component-specific css imports
+@import './components/FileItem/index.scss';
+
 // transitions //
 // transitions //
 
 
 .uppy-transition-slideDownUp-enter {
 .uppy-transition-slideDownUp-enter {
@@ -146,7 +149,7 @@
   opacity: 0;
   opacity: 0;
 }
 }
 
 
-  .uppy-Dashboard--isInnerWrapVisible .uppy-Dashboard-innerWrap{
+  .uppy-Dashboard--isInnerWrapVisible .uppy-Dashboard-innerWrap {
     opacity: 1;
     opacity: 1;
   }
   }
 
 
@@ -310,7 +313,7 @@
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
-  padding: 12px 20px;
+  padding: 12px 15px;
   line-height: 1;
   line-height: 1;
   text-align: center;
   text-align: center;
 
 
@@ -333,7 +336,7 @@
 
 
   .uppy-DashboardTab-btn:active,
   .uppy-DashboardTab-btn:active,
   .uppy-DashboardTab-btn:focus {
   .uppy-DashboardTab-btn:focus {
-    background-color: darken($gray-100--highlighted, 2%);
+    background-color: $highlight;
     outline: none;
     outline: none;
   }
   }
 
 
@@ -429,6 +432,8 @@
 
 
 .uppy-DashboardContent-back {
 .uppy-DashboardContent-back {
   @include reset-button;
   @include reset-button;
+  @include highlight-focus;
+  border-radius: 3px;
   display: inline-block;
   display: inline-block;
   font-size: 12px;
   font-size: 12px;
   font-weight: 400;
   font-weight: 400;
@@ -436,11 +441,6 @@
   color: $blue;
   color: $blue;
   padding: 7px 6px;
   padding: 7px 6px;
   margin-left: -6px;
   margin-left: -6px;
-  border-radius: 3px;
-
-  &:hover {
-    color: darken($blue, 12%);
-  }
 
 
   .uppy-size--md & {
   .uppy-size--md & {
     font-size: 14px;
     font-size: 14px;
@@ -449,23 +449,41 @@
 
 
 .uppy-DashboardContent-addMore {
 .uppy-DashboardContent-addMore {
   @include reset-button;
   @include reset-button;
+  @include highlight-focus;
+  border-radius: 3px;
   display: inline-block;
   display: inline-block;
   font-weight: 500;
   font-weight: 500;
   cursor: pointer;
   cursor: pointer;
   color: $blue;
   color: $blue;
-  width: 27px;
-  height: 27px;
-  padding: 6px;
-  margin-right: -6px;
-  border-radius: 3px;
+  width: 29px;
+  height: 29px;
+  padding: 7px 8px;
+  margin-right: -5px;
 
 
-  &:hover {
-    color: darken($blue, 12%)
+  .uppy-size--md & {
+    font-size: 14px;
+    width: auto;
+    height: auto;
+    margin-right: -8px;
   }
   }
 }
 }
 
 
   .uppy-DashboardContent-addMore svg {
   .uppy-DashboardContent-addMore svg {
-    vertical-align: text-top;
+    vertical-align: baseline;
+    margin-right: 4px;
+
+    .uppy-size--md & {
+      width: 11px;
+      height: 11px;
+    }
+  }
+
+  .uppy-DashboardContent-addMoreCaption {
+    display: none;
+
+    .uppy-size--md & {
+      display: inline;
+    }
   }
   }
 
 
 .uppy-DashboardContent-panel {
 .uppy-DashboardContent-panel {
@@ -596,8 +614,8 @@
   font-size: 16px;
   font-size: 16px;
 }
 }
 
 
-.uppy-Dashboard.uppy-Dashboard--isDraggingOver{
-  .uppy-Dashboard-dropFilesHereHint{
+.uppy-Dashboard.uppy-Dashboard--isDraggingOver {
+  .uppy-Dashboard-dropFilesHereHint {
     visibility: visible;
     visibility: visible;
   }
   }
   .uppy-DashboardContent-bar,
   .uppy-DashboardContent-bar,
@@ -664,152 +682,6 @@ a.uppy-Dashboard-poweredBy {
   vertical-align: text-top;
   vertical-align: text-top;
 }
 }
 
 
-.uppy-DashboardItem {
-  list-style: none;
-  margin: 10px 0;
-  position: relative;
-  display: flex;
-  align-items: center;
-  border-bottom: 1px solid $gray-200;
-  padding-bottom: 10px;
-  padding-left: 10px;
-
-  $rl-margin: 15px;
-  .uppy-size--md & {
-    float: left;
-    margin: 5px $rl-margin;
-    width: calc(33.333% - #{$rl-margin} - #{$rl-margin});
-    height: 215px;
-
-    flex-direction: column;
-    background-color: initial;
-    border: 0;
-    border-bottom: none;
-    padding-bottom: 0;
-    padding-left: 0;
-  }
-
-  .uppy-size--lg & {
-    margin: 5px $rl-margin;
-    width: calc(25% - #{$rl-margin} - #{$rl-margin});
-    height: 190px;
-  }
-
-  .uppy-size--xl & {
-    width: calc(20% - #{$rl-margin} - #{$rl-margin});
-    height: 210px;
-  }
-}
-
-.uppy-DashboardItem-preview {
-  width: 50px;
-  height: 50px;
-  border-bottom: 0;
-  position: relative;
-  flex-shrink: 0;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-
-  .uppy-size--md & {
-    width: 100%;
-    height: 140px;
-    border: 0;
-  }
-
-  .uppy-size--lg & {
-    height: 120px;
-  }
-
-  .uppy-size--xl & {
-    height: 140px;
-  }
-}
-
-.uppy-DashboardItem-previewLink {
-  position: absolute;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  z-index: $zIndex-3;
-  &:focus{
-    box-shadow: inset 0px 0px 0px 4px rgb(59, 153, 252);
-  }
-}
-
-.uppy-DashboardItem-sourceIcon {
-  display: inline-block;
-  vertical-align: bottom;
-  color: $gray-500;
-}
-
-.uppy-DashboardItem-sourceIcon svg,
-.uppy-DashboardItem-sourceIcon svg * {
-  max-width: 100%;
-  max-height: 100%;
-  display: inline-block;
-  vertical-align: text-bottom;
-  overflow: hidden;
-  fill: currentColor;
-  width: 11px;
-  height: 12px;
-}
-
-.uppy-DashboardItem-previewInnerWrap {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  position: relative;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  flex-direction: column;
-  box-shadow: 0 0 2px 0 rgba($black, 0.4);
-  border-radius: 3px;
-
-  .uppy-size--md & {
-    box-shadow: 0 1px 3px rgba($black, 0.2);
-  }
-}
-
-  .uppy-DashboardItem-previewInnerWrap:after {
-    content: '';
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    background-color: rgba($black, 0.65) /* no !important */;
-    display: none;
-    z-index: $zIndex-2;
-  }
-
-
-.uppy-DashboardItem-preview img {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  // Fixes file previews being partially invisible in safari (for some pics only).
-  // (https://stackoverflow.com/a/27971913/3192470)
-  transform: translateZ(0);
-  // We need a repeated border-radius because of the transform.
-  border-radius: 3px;
-}
-
-
-.uppy-DashboardItem-previewIconWrap {
-  height: 76px;
-  max-height: 75%;
-  position: relative;
-}
-
-.uppy-DashboardItem-previewIconBg {
-  width: 100%;
-  height: 100%;
-  filter: drop-shadow(rgba($black, 0.1) 0px 1px 1px);
-}
-
 .uppy-DashboardItem-previewIcon {
 .uppy-DashboardItem-previewIcon {
   width: 25px;
   width: 25px;
   height: 25px;
   height: 25px;
@@ -830,342 +702,16 @@ a.uppy-Dashboard-poweredBy {
   }
   }
 }
 }
 
 
-.uppy-DashboardItem-previewType {
-  position: absolute;
-  bottom: 14px;
-  left: 50%;
-  transform: translate(-50%, 0);
-  text-transform: uppercase;
-  font-size: 9px;
-  letter-spacing: 1px;
-  color: $gray-700;
-  z-index: $zIndex-1;
-  user-select: none;
-}
-
-.uppy-DashboardItem-info {
-  padding-left: 15px;
-  position: relative;
-  max-width: 65%;
-
-  .uppy-size--md & {
-    width: 100%;
-    max-width: 100%;
-    flex: 1;
-    padding: 8px 0 0;
-    border-top: 0;
-  }
-}
-
-.uppy-DashboardItem-name {
-  font-size: 12px;
-  line-height: 1.3;
-  font-weight: 500;
-  margin: 0;
-  padding: 0;
-  margin-bottom: 5px;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  overflow: hidden;
-
-  .uppy-size--md & {
-    word-break: break-all;
-    white-space: normal;
-    overflow: initial;
-  }
-}
-
-.uppy-DashboardItem-name a {
-  text-decoration: none;
-  color: $gray-800;
-}
-
-.uppy-DashboardItem-status {
-  font-size: 11px;
-  line-height: 1.3;
-  font-weight: normal;
-  color: $gray-600;
-  margin-bottom: 4px;
-}
-
-.uppy-DashboardItem-statusSize {
-  display: inline-block;
-  vertical-align: bottom;
-  text-transform: uppercase;
-}
-
-.uppy-DashboardItem-edit,
-.uppy-DashboardItem-copyLink {
-  display: inline-block;
-  vertical-align: bottom;
-  cursor: pointer;
-  font-family: inherit;
-  font-size: inherit;
-  line-height: inherit;
-  color: inherit;
-
-  &:hover {
-    text-decoration: underline;
-  }
-}
-
-span.uppy-DashboardItem-statusbar-dot{
-  display: inline-block;
-  color: $gray-600;
-  &:last-child{
-    display: none;
-  }
-}
-
-.uppy-DashboardItem-statusSize,
-.uppy-DashboardItem-edit,
-.uppy-DashboardItem-copyLink,
-.uppy-DashboardItem-sourceIcon {
+.uppy-DashboardItem-previewIconWrap {
+  height: 76px;
+  max-height: 75%;
   position: relative;
   position: relative;
-  margin-left: 3px;
-  margin-right: 3px;
-
-  padding-left: 3px;
-  padding-right: 3px;
-
-  &:first-child{
-    margin-left: 0;
-    padding-left: 0;
-  }
-}
-
-.uppy-DashboardItem-action {
-  position: absolute;
-  top: 18px;
-  right: 10px;
-  z-index: $zIndex-3;
-
-  .uppy-size--md & {
-    top: -8px;
-    right: -8px;
-  }
-}
-
-.uppy-DashboardItem-remove {
-  @include reset-button;
-  cursor: pointer;
-  color: $gray-900;
-  width: 20px;
-  height: 20px;
-  padding: 1px;
-  opacity: 0.9;
-
-  &:hover {
-    opacity: 1;
-  }
-}
-
-  .uppy-DashboardItem.is-inprogress:not(.is-resumable) .uppy-DashboardItem-remove {
-    display: none;
-  }
-
-
-.uppy-DashboardItem-progress {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  z-index: $zIndex-3;
-  color: $white;
-  text-align: center;
-  width: 120px;
-  display: none;
-  transition: all .35 ease;
-}
-
-  .uppy-DashboardItem.is-complete .uppy-DashboardItem-progress {
-    transform: initial;
-    top: -9px;
-    right: -8px;
-    left: initial;
-    width: auto;
-  }
-
-  .uppy-DashboardItem.is-inprogress .uppy-DashboardItem-progress,
-  .uppy-DashboardItem.is-complete .uppy-DashboardItem-progress,
-  .uppy-DashboardItem.is-error .uppy-DashboardItem-progress {
-    display: block;
-  }
-
-.uppy-DashboardItem-progressIndicator {
-  @include reset-button;
-  display: inline-block;
-  width: 38px;
-  height: 38px;
-  opacity: 0.9;
-
-  .uppy-size--md & {
-    width: 55px;
-    height: 55px;
-  }
 }
 }
 
 
-  button.uppy-DashboardItem-progressIndicator {
-    cursor: pointer;
-  }
-
-  .uppy-DashboardItem.is-error .uppy-DashboardItem-progressIndicator {
-    width: 18px;
-    height: 18px;
-
-    .uppy-size--md & {
-      width: 28px;
-      height: 28px;
-    }
-  }
-
-  .uppy-DashboardItem.is-complete .uppy-DashboardItem-progressIndicator {
-    width: 18px;
-    height: 18px;
-    opacity: 1;
-
-    .uppy-size--md & {
-      width: 22px;
-      height: 22px;
-    }
-  }
-
-.uppy-DashboardItem-progressInfo {
-  font-size: 9px;
-  line-height: 1;
-  font-weight: 500;
-  height: 10px;
-  display: none;
-  position: absolute;
-  bottom: -10px;
-  left: 0;
-  width: 100%;
-  text-shadow: 0 1px 0 rgba($black, 0.3);
-
-  .uppy-size--md & {
-    display: block;
-  }
-}
-
-.UppyIcon-progressCircle {
+.uppy-DashboardItem-previewIconBg {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
-}
-
-.uppy-DashboardItem .bg {
-  stroke: rgba($white, 0.4);
-  opacity: 0;
-}
-
-.uppy-DashboardItem .progress {
-  stroke: $white;
-  transition: stroke-dashoffset .5s ease-out;
-  opacity: 0;
-}
-
-.uppy-DashboardItem .play {
-  stroke: $white;
-  fill: $white;
-  opacity: 0;
-  transition: all 0.2s;
-  display: none;
-}
-
-.uppy-DashboardItem .cancel {
-  fill: $white;
-  opacity: 0;
-  transition: all 0.2s;
-}
-
-.uppy-DashboardItem .pause {
-  stroke: $white;
-  fill: $white;
-  opacity: 0;
-  transition: all 0.2s;
-  display: none;
-}
-
-.uppy-DashboardItem.is-error .retry {
-  fill: $white;
-}
-
-.uppy-DashboardItem.is-resumable {
-  .pause, .play { display: block; }
-  .cancel { display: none; }
-}
-
-.UppyIcon-progressCircle .check {
-  opacity: 0;
-  fill: $white;
-  transition: all 0.2s;
-}
-
-.uppy-DashboardItem.is-inprogress {
-  .bg, .progress, .pause, .cancel {
-    opacity: 1;
-  }
-
-  .uppy-DashboardItem-previewInnerWrap:after {
-    display: block;
-  }
-}
-
-.uppy-DashboardItem.is-error {
-  .uppy-DashboardItem-previewInnerWrap:after {
-    display: block;
-  }
-}
-
-.uppy-DashboardItem.is-paused {
-  .pause {
-    opacity: 0;
-  }
-  .play {
-    opacity: 1;
-  }
-}
-
-.uppy-DashboardItem.is-noIndividualCancellation {
-  .uppy-DashboardItem-progressIndicator {
-    cursor: default;
-  }
-
-  .cancel {
-    display: none;
-  }
-}
-
-.uppy-DashboardItem.is-processing .uppy-DashboardItem-progress {
-  opacity: 0;
-}
-
-.uppy-DashboardItem.is-complete {
-  .uppy-DashboardItem-progressIndicator {
-    cursor: default;
-  }
-
-  .progress {
-    stroke: $green;
-    fill: $green;
-    opacity: 1;
-  }
-
-  .check {
-    opacity: 1;
-  }
-}
-
-.uppy-DashboardItem-progressNum {
-  position: relative;
-  z-index: $zIndex-2;
-}
-
-.uppy-DashboardItem-progressInner {
-  height: 15px;
-  background-color: $blue;
-  position: absolute;
-  top: 0;
-  left: 0;
+  filter: drop-shadow(rgba($black, 0.1) 0px 1px 1px);
 }
 }
 
 
 .uppy-Dashboard-actions {
 .uppy-Dashboard-actions {
@@ -1175,21 +721,15 @@ span.uppy-DashboardItem-statusbar-dot{
   align-items: center;
   align-items: center;
   padding: 0 15px;
   padding: 0 15px;
   background-color: $gray-50;
   background-color: $gray-50;
-}
-
-  .uppy-size--md .uppy-Dashboard-actions {
+  .uppy-size--md & {
     height: 65px;
     height: 65px;
   }
   }
+}
 
 
 .uppy-Dashboard-actionsBtn {
 .uppy-Dashboard-actionsBtn {
   margin-right: 10px;
   margin-right: 10px;
 }
 }
 
 
-.uppy-Dashboard-pauseResume .UppyIcon {
-  width: 100%;
-  height: 100%;
-}
-
 .uppy-Dashboard-upload {
 .uppy-Dashboard-upload {
   position: relative;
   position: relative;
   width: 50px;
   width: 50px;
@@ -1269,7 +809,7 @@ span.uppy-DashboardItem-statusbar-dot{
   position: relative;
   position: relative;
 }
 }
 
 
-.uppy-DashboardFileCard-preview img {
+.uppy-DashboardFileCard-preview img.uppy-DashboardItem-previewImg {
   box-shadow: 0px 3px 20px rgba($black, 0.15);
   box-shadow: 0px 3px 20px rgba($black, 0.15);
   max-width: 90%;
   max-width: 90%;
   max-height: 90%;
   max-height: 90%;

+ 24 - 7
packages/@uppy/dashboard/src/utils/truncateString.js

@@ -1,9 +1,26 @@
-module.exports = function truncateString (str, length) {
-  if (str.length > length) {
-    return str.substr(0, length / 2) + '...' + str.substr(str.length - length / 4, str.length)
-  }
-  return str
+/**
+ * Truncates a string to the given number of chars (maxLength) by inserting '...' in the middle of that string.
+ * Partially taken from https://stackoverflow.com/a/5723274/3192470.
+ *
+ * @param {string} string - string to be truncated
+ * @param {number} maxLength - maximum size of the resulting string
+ * @returns {string}
+ */
+module.exports = function truncateString (string, maxLength) {
+  const separator = '...'
+
+  // Return original string if it's already shorter than maxLength
+  if (string.length <= maxLength) {
+    return string
+  // Return truncated substring without '...' if string can't be meaningfully truncated
+  } else if (maxLength <= separator.length) {
+    return string.substr(0, maxLength)
+  // Return truncated string divided in half by '...'
+  } else {
+    const charsToShow = maxLength - separator.length
+    const frontChars = Math.ceil(charsToShow / 2)
+    const backChars = Math.floor(charsToShow / 2)
 
 
-  // more precise version if needed
-  // http://stackoverflow.com/a/831583
+    return string.substr(0, frontChars) + separator + string.substr(string.length - backChars)
+  }
 }
 }

+ 26 - 11
packages/@uppy/dashboard/src/utils/truncateString.test.js

@@ -1,16 +1,31 @@
 const truncateString = require('./truncateString')
 const truncateString = require('./truncateString')
 
 
 describe('truncateString', () => {
 describe('truncateString', () => {
-  it('should truncate the string by the specified amount', () => {
-    expect(truncateString('abcdefghijkl', 10)).toEqual('abcde...jkl')
-    expect(truncateString('abcdefghijkl', 9)).toEqual('abcd...jkl')
-    expect(truncateString('abcdefghijkl', 8)).toEqual('abcd...kl')
-    expect(truncateString('abcdefghijkl', 7)).toEqual('abc...kl')
-    expect(truncateString('abcdefghijkl', 6)).toEqual('abc...kl')
-    expect(truncateString('abcdefghijkl', 5)).toEqual('ab...kl')
-    expect(truncateString('abcdefghijkl', 4)).toEqual('ab...l')
-    expect(truncateString('abcdefghijkl', 3)).toEqual('a...l')
-    expect(truncateString('abcdefghijkl', 2)).toEqual('a...l')
-    expect(truncateString('abcdefghijkl', 1)).toEqual('...l')
+  it('should truncate the string to the length', () => {
+    expect(truncateString('abcdefghijkl', 14)).toEqual('abcdefghijkl')
+    expect(truncateString('abcdefghijkl', 13)).toEqual('abcdefghijkl')
+    expect(truncateString('abcdefghijkl', 12)).toEqual('abcdefghijkl')
+    expect(truncateString('abcdefghijkl', 11)).toEqual('abcd...ijkl')
+    expect(truncateString('abcdefghijkl', 10)).toEqual('abcd...jkl')
+    expect(truncateString('abcdefghijkl', 9)).toEqual('abc...jkl')
+    expect(truncateString('abcdefghijkl', 8)).toEqual('abc...kl')
+    expect(truncateString('abcdefghijkl', 7)).toEqual('ab...kl')
+    expect(truncateString('abcdefghijkl', 6)).toEqual('ab...l')
+    expect(truncateString('abcdefghijkl', 5)).toEqual('a...l')
+    expect(truncateString('abcdefghijkl', 4)).toEqual('a...')
+    expect(truncateString('abcdefghijkl', 3)).toEqual('abc')
+    expect(truncateString('abcdefghijkl', 2)).toEqual('ab')
+    expect(truncateString('abcdefghijkl', 1)).toEqual('a')
+    expect(truncateString('abcdefghijkl', 0)).toEqual('')
+  })
+
+  it('should not truncate the string if it is already short enough', () => {
+    expect(truncateString('hello world', 100)).toEqual('hello world')
+    expect(truncateString('hello world', 11)).toEqual('hello world')
+  })
+
+  it('should not truncate the string if it is too short to be meaningfully truncated', () => {
+    expect(truncateString('abc', 2)).toEqual('ab')
+    expect(truncateString('abc', 1)).toEqual('a')
   })
   })
 })
 })

+ 1 - 0
packages/@uppy/locales/src/de_DE.js

@@ -8,6 +8,7 @@ de_DE.strings = {
   authenticateWith: 'Mit %{pluginName} verbinden',
   authenticateWith: 'Mit %{pluginName} verbinden',
   authenticateWithTitle: 'Bitte authentifizieren Sie sich mit %{pluginName}, um Dateien auszuwählen',
   authenticateWithTitle: 'Bitte authentifizieren Sie sich mit %{pluginName}, um Dateien auszuwählen',
   back: 'Zurück',
   back: 'Zurück',
+  addMore: 'Dateien hinzufügen',
   browse: 'Suche',
   browse: 'Suche',
   cancel: 'Abbrechen',
   cancel: 'Abbrechen',
   cancelUpload: 'Upload abbrechen',
   cancelUpload: 'Upload abbrechen',

+ 1 - 0
packages/@uppy/locales/src/en_US.js

@@ -1,6 +1,7 @@
 const en_US = {}
 const en_US = {}
 
 
 en_US.strings = {
 en_US.strings = {
+  addMore: 'Add more',
   addMoreFiles: 'Add more files',
   addMoreFiles: 'Add more files',
   addingMoreFiles: 'Adding more files',
   addingMoreFiles: 'Adding more files',
   allowAccessDescription: 'In order to take pictures or record video with your camera, please allow camera access for this site.',
   allowAccessDescription: 'In order to take pictures or record video with your camera, please allow camera access for this site.',

+ 1 - 0
packages/@uppy/locales/src/es_ES.js

@@ -8,6 +8,7 @@ es_ES.strings = {
   authenticateWith: 'Conectar a %{pluginName}',
   authenticateWith: 'Conectar a %{pluginName}',
   authenticateWithTitle: 'Por favor autentícate con %{pluginName} para seleccionar archivos',
   authenticateWithTitle: 'Por favor autentícate con %{pluginName} para seleccionar archivos',
   back: 'Atrás',
   back: 'Atrás',
+  addMore: 'Agregar más',
   browse: 'navegar',
   browse: 'navegar',
   cancel: 'Cancelar',
   cancel: 'Cancelar',
   cancelUpload: 'Cancelar subida',
   cancelUpload: 'Cancelar subida',

+ 1 - 0
packages/@uppy/locales/src/fa_IR.js

@@ -8,6 +8,7 @@ fa_IR.strings = {
   authenticateWith: 'در حال اتصال به %{pluginName}',
   authenticateWith: 'در حال اتصال به %{pluginName}',
   authenticateWithTitle: 'احراز هویت %{pluginName} برای انتخاب فایل ضروریست!',
   authenticateWithTitle: 'احراز هویت %{pluginName} برای انتخاب فایل ضروریست!',
   back: 'بازگشت',
   back: 'بازگشت',
+  addMore: 'اضافه کردن بیشتر',
   browse: 'از رایانه',
   browse: 'از رایانه',
   cancel: 'انصراف',
   cancel: 'انصراف',
   cancelUpload: 'لغو بارگذاری',
   cancelUpload: 'لغو بارگذاری',

+ 1 - 0
packages/@uppy/locales/src/fr_FR.js

@@ -8,6 +8,7 @@ fr_FR.strings = {
   authenticateWith: 'Se connecter à %{pluginName}',
   authenticateWith: 'Se connecter à %{pluginName}',
   authenticateWithTitle: 'Veuillez vous authentifier avec %{pluginName} pour sélectionner les fichiers',
   authenticateWithTitle: 'Veuillez vous authentifier avec %{pluginName} pour sélectionner les fichiers',
   back: 'Retour',
   back: 'Retour',
+  addMore: 'Ajouter d\'autres',
   browse: 'naviguer',
   browse: 'naviguer',
   cancel: 'Annuler',
   cancel: 'Annuler',
   cancelUpload: 'Annuler téléchargement',
   cancelUpload: 'Annuler téléchargement',

+ 1 - 0
packages/@uppy/locales/src/it_IT.js

@@ -8,6 +8,7 @@ it_IT.strings = {
   authenticateWith: 'Connetti a %{pluginName}',
   authenticateWith: 'Connetti a %{pluginName}',
   authenticateWithTitle: 'Autenticati con %{pluginName} per selezionare i file',
   authenticateWithTitle: 'Autenticati con %{pluginName} per selezionare i file',
   back: 'Indietro',
   back: 'Indietro',
+  addMore: 'Aggiungi più',
   browse: 'sfoglia',
   browse: 'sfoglia',
   cancel: 'Annulla',
   cancel: 'Annulla',
   cancelUpload: 'Annulla upload',
   cancelUpload: 'Annulla upload',

+ 1 - 0
packages/@uppy/locales/src/nl_NL.js

@@ -8,6 +8,7 @@ nl_NL.strings = {
   authenticateWith: 'Verbinden met %{pluginName}',
   authenticateWith: 'Verbinden met %{pluginName}',
   authenticateWithTitle: 'Verbindt met %{pluginName} om bestanden te selecteren',
   authenticateWithTitle: 'Verbindt met %{pluginName} om bestanden te selecteren',
   back: 'Terug',
   back: 'Terug',
+  addMore: 'Meer toevoegen',
   browse: 'blader',
   browse: 'blader',
   cancel: 'Annuleer',
   cancel: 'Annuleer',
   cancelUpload: 'Annuleer upload',
   cancelUpload: 'Annuleer upload',

+ 2 - 1
packages/@uppy/locales/src/ru_RU.js

@@ -8,6 +8,7 @@ ru_RU.strings = {
   authenticateWithTitle: 'Пожалуйста, авторизуйтесь в %{pluginName} чтобы выбрать файлы',
   authenticateWithTitle: 'Пожалуйста, авторизуйтесь в %{pluginName} чтобы выбрать файлы',
   authenticateWith: 'Подключиться к %{pluginName}',
   authenticateWith: 'Подключиться к %{pluginName}',
   back: 'Назад',
   back: 'Назад',
+  addMore: 'Добавить еще',
   browse: 'выберите',
   browse: 'выберите',
   cancel: 'Отменить',
   cancel: 'Отменить',
   cancelUpload: 'Отменить загрузку',
   cancelUpload: 'Отменить загрузку',
@@ -79,7 +80,7 @@ ru_RU.strings = {
   resumeUpload: 'Продолжить загрузку',
   resumeUpload: 'Продолжить загрузку',
   retry: 'Повторить попытку',
   retry: 'Повторить попытку',
   retryUpload: 'Повторить попытку загрузки',
   retryUpload: 'Повторить попытку загрузки',
-  saveChanges: 'Сохранить изменения',
+  saveChanges: 'Сохранить',
   selectXFiles: {
   selectXFiles: {
     '0': 'Выбрать %{smart_count} файл',
     '0': 'Выбрать %{smart_count} файл',
     '1': 'Выбрать %{smart_count} файла',
     '1': 'Выбрать %{smart_count} файла',

+ 1 - 0
packages/@uppy/locales/src/zh_CN.js

@@ -8,6 +8,7 @@ zh_CN.strings = {
   authenticateWith: '连接到%{pluginName}',
   authenticateWith: '连接到%{pluginName}',
   authenticateWithTitle: '请使用%{pluginName}进行身份验证以选择文件',
   authenticateWithTitle: '请使用%{pluginName}进行身份验证以选择文件',
   back: '返回',
   back: '返回',
+  addMore: '添加更多',
   browse: '浏览',
   browse: '浏览',
   cancel: '取消',
   cancel: '取消',
   cancelUpload: '取消上传',
   cancelUpload: '取消上传',

+ 1 - 0
packages/@uppy/locales/src/zh_TW.js

@@ -8,6 +8,7 @@ zh_TW.strings = {
   authenticateWith: '連接到%{pluginName}',
   authenticateWith: '連接到%{pluginName}',
   authenticateWithTitle: '請使用%{pluginName}進行身份驗證以選擇文件',
   authenticateWithTitle: '請使用%{pluginName}進行身份驗證以選擇文件',
   back: '返回',
   back: '返回',
+  addMore: '添加更多',
   browse: '瀏覽',
   browse: '瀏覽',
   cancel: '取消',
   cancel: '取消',
   cancelUpload: '取消上傳',
   cancelUpload: '取消上傳',