Bring back StatusBar and DragDrop (#5931)

While the frameworks have good alternatives now with new components and
hooks, Uppy is also popular in 'vanilla' JS setups without frameworks
using the CDN bundle. Consumers of this were left with no viable
alternative so it's sensible to bring back status bar and drag drop for
the CDN publish (at least until we also publish/document using hooks via
CDN). The framework packages don't really need this, as the alternatives
are viable there.

- Bring back `@uppy/status-bar` and `@uppy/drag-drop` from git tag
`4.18.1` (latest release before 5.0)
- Put exports maps on both packages
- Put both packages in the CDN bundle
- Version appropriately with changesets
- Override existing locale keys. Unfortunately now that status-bar was
merged into dashboard, the keys need to exist in both places but our
tooling was setup to error when the same keys are found. Now it just
overrides the existing key (to the same value in this case)
This commit is contained in:
Merlijn Vos 2025-08-27 14:56:04 +02:00 committed by GitHub
parent 057c1aeefa
commit 3290864cf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2777 additions and 6 deletions

View file

@ -0,0 +1,9 @@
---
"@uppy/status-bar": major
"@uppy/drag-drop": major
---
All packages now have export maps. This is a breaking change in two cases:
1. The css imports have changed from `@uppy[package]/dist/styles.min.css` to `@uppy[package]/css/styles.min.css`
2. You were importing something that wasn't exported from the root, for instance `@uppy/core/lib/foo.js`. You can now only import things we explicitly exported.

View file

@ -0,0 +1,5 @@
---
"uppy": minor
---
Bring back StatusBar and DragDrop into the CDN bundle

View file

@ -0,0 +1 @@
tsconfig.*

View file

@ -0,0 +1,139 @@
# @uppy/drag-drop
## 4.2.2
### Patch Changes
- 1b1a9e3: Define "files" in package.json
- Updated dependencies [1b1a9e3]
- @uppy/utils@6.2.2
- @uppy/core@4.5.2
## 4.2.0
### Minor Changes
- 0c24c5a: Use TypeScript compiler instead of Babel
### Patch Changes
- Updated dependencies [0c24c5a]
- Updated dependencies [0c24c5a]
- @uppy/core@4.5.0
- @uppy/utils@6.2.0
## 4.1.3
Released: 2025-05-18
Included in: Uppy v4.16.0
- @uppy/audio,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/image-editor,@uppy/instagram,@uppy/onedrive,@uppy/remote-sources,@uppy/screen-capture,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/webcam,@uppy/webdav,@uppy/zoom: ts: make locale strings optional (Merlijn Vos / #5728)
## 4.1.0
Released: 2025-01-06
Included in: Uppy v4.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/webdav,@uppy/xhr-upload,@uppy/zoom: Remove "paths" from all tsconfig's (Merlijn Vos / #5572)
## 4.0.5
Released: 2024-12-05
Included in: Uppy v4.8.0
- @uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: cleanup tsconfig (Mikael Finstad / #5520)
## 4.0.4
Released: 2024-10-31
Included in: Uppy v4.6.0
- @uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react-native,@uppy/react,@uppy/redux-dev-tools,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Fix links (Anthony Veaudry / #5492)
## 4.0.3
Released: 2024-10-15
Included in: Uppy v4.5.0
- @uppy/dashboard,@uppy/drag-drop,@uppy/file-input: `.handleInputChange()` - use `.currentTarget`; clear the input using `''` (Evgenia Karunus / #5381)
## 4.0.2
Released: 2024-08-15
Included in: Uppy v4.1.1
- @uppy/dashboard,@uppy/drag-drop,@uppy/file-input: Transform the `accept` prop into a string everywhere (Evgenia Karunus / #5380)
## 4.0.1
Released: 2024-07-15
Included in: Uppy v4.0.1
- @uppy/dashboard,@uppy/drag-drop,@uppy/drop-target: `<Dashboard/>`, `<DragDrop/>`, `drop-target` - new anti-flickering solution (Evgenia Karunus / #5326)
## 4.0.0-beta.1
Released: 2024-03-28
Included in: Uppy v4.0.0-beta.1
- @uppy/drag-drop: refine type of private variables (Antoine du Hamel / #5026)
- @uppy/drag-drop,@uppy/progress-bar: add missing exports (Antoine du Hamel / #5009)
- @uppy/drag-drop: refactor to TypeScript (Antoine du Hamel / #4983)
## 3.1.1
Released: 2024-07-02
Included in: Uppy v3.27.2
- docs,@uppy/drag-drop: `uppy.io/docs` - fix typos/broken links (Evgenia Karunus / #5296)
## 3.1.0
Released: 2024-03-27
Included in: Uppy v3.24.0
- @uppy/drag-drop: refine type of private variables (Antoine du Hamel / #5026)
- @uppy/drag-drop,@uppy/progress-bar: add missing exports (Antoine du Hamel / #5009)
- @uppy/drag-drop: refactor to TypeScript (Antoine du Hamel / #4983)
## 3.0.1
Released: 2022-09-25
Included in: Uppy v3.1.0
- @uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/companion,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/redux-dev-tools,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092)
## 3.0.0
Released: 2022-08-22
Included in: Uppy v3.0.0
- Switch to ESM
## 2.1.1
Released: 2022-05-30
Included in: Uppy v2.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763)
## 2.0.7
Released: 2022-04-27
Included in: Uppy v2.9.4
- @uppy/drag-drop: refactor to ESM (Antoine du Hamel / #3647)
## 2.0.6
Released: 2022-01-10
Included in: Uppy v2.4.0
- @uppy/drag-drop: fix `undefined is not a function` TypeError (Antoine du Hamel / #3397)
## 2.0.5
Released: 2021-12-07
Included in: Uppy v2.3.0
- @uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive,@uppy/image-editor,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/url,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Refactor locale scripts & generate types and docs (Merlijn Vos / #3276)

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Transloadit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,47 @@
# @uppy/drag-drop
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
[![npm version](https://img.shields.io/npm/v/@uppy/drag-drop.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/drag-drop)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/CI/badge.svg)
![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg)
![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg)
Droppable zone UI for Uppy. Drag and drop files into it to upload.
**[Read the docs](https://uppy.io/docs/drag-drop/)**
Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
a versatile file encoding service.
## Example
```js
import Uppy from '@uppy/core'
import DragDrop from '@uppy/drag-drop'
const uppy = new Uppy()
uppy.use(DragDrop, {
target: '#upload',
})
```
## Installation
```bash
$ npm install @uppy/drag-drop
```
Alternatively, you can also use this plugin in a pre-built bundle from
Transloadits CDN: Smart CDN. In that case `Uppy` will attach itself to the
global `window.Uppy` object. See the
[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
## Documentation
Documentation for this plugin can be found on the
[Uppy website](https://uppy.io/docs/drag-drop/).
## License
[The MIT License](./LICENSE).

View file

@ -0,0 +1,61 @@
{
"name": "@uppy/drag-drop",
"description": "Droppable zone UI for Uppy. Drag and drop files into it to upload.",
"version": "4.2.2",
"license": "MIT",
"main": "lib/index.js",
"style": "dist/style.min.css",
"type": "module",
"sideEffects": [
"*.css"
],
"scripts": {
"build": "tsc --build tsconfig.build.json",
"build:css": "sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",
"typecheck": "tsc --build"
},
"keywords": [
"file uploader",
"uppy",
"uppy-plugin",
"drag-drop",
"drag",
"drop",
"dropzone",
"upload"
],
"homepage": "https://uppy.io",
"bugs": {
"url": "https://github.com/transloadit/uppy/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
},
"files": [
"src",
"lib",
"dist",
"CHANGELOG.md"
],
"exports": {
".": "./lib/index.js",
"./css/style.css": "./dist/style.css",
"./css/style.min.css": "./dist/style.min.css",
"./package.json": "./package.json"
},
"dependencies": {
"@uppy/utils": "workspace:^",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
},
"devDependencies": {
"cssnano": "^7.0.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"sass": "^1.89.2",
"typescript": "^5.8.3"
}
}

View file

@ -0,0 +1,245 @@
import type {
Body,
DefinePluginOpts,
Meta,
UIPluginOptions,
Uppy,
} from '@uppy/core'
import { UIPlugin } from '@uppy/core'
import type { LocaleStrings } from '@uppy/utils'
import { getDroppedFiles, isDragDropSupported, toArray } from '@uppy/utils'
import type { ComponentChild } from 'preact'
import type { TargetedEvent } from 'preact/compat'
import packageJson from '../package.json' with { type: 'json' }
import locale from './locale.js'
export interface DragDropOptions extends UIPluginOptions {
inputName?: string
allowMultipleFiles?: boolean
width?: string | number
height?: string | number
note?: string
onDragOver?: (event: DragEvent) => void
onDragLeave?: (event: DragEvent) => void
onDrop?: (event: DragEvent) => void
locale?: LocaleStrings<typeof locale>
}
const defaultOptions = {
inputName: 'files[]',
width: '100%',
height: '100%',
} satisfies Partial<DragDropOptions>
/**
* Drag & Drop plugin
*
*/
export default class DragDrop<M extends Meta, B extends Body> extends UIPlugin<
DefinePluginOpts<DragDropOptions, keyof typeof defaultOptions>,
M,
B
> {
static VERSION = packageJson.version
// Check for browser dragDrop support
private isDragDropSupported = isDragDropSupported()
private fileInputRef!: HTMLInputElement
constructor(uppy: Uppy<M, B>, opts?: DragDropOptions) {
super(uppy, {
...defaultOptions,
...opts,
})
this.type = 'acquirer'
this.id = this.opts.id || 'DragDrop'
this.title = 'Drag & Drop'
this.defaultLocale = locale
this.i18nInit()
}
private addFiles = (files: File[]) => {
const descriptors = files.map((file) => ({
source: this.id,
name: file.name,
type: file.type,
data: file,
meta: {
// path of the file relative to the ancestor directory the user selected.
// e.g. 'docs/Old Prague/airbnb.pdf'
relativePath: (file as any).relativePath || null,
} as any as M,
}))
try {
this.uppy.addFiles(descriptors)
} catch (err) {
this.uppy.log(err as any)
}
}
private onInputChange = (event: TargetedEvent<HTMLInputElement, Event>) => {
const files = toArray(event.currentTarget.files || [])
if (files.length > 0) {
this.uppy.log('[DragDrop] Files selected through input')
this.addFiles(files)
}
// Clear the input so that Chrome can detect file section when the same file is repeatedly selected
// (see https://github.com/transloadit/uppy/issues/768#issuecomment-2264902758)
event.currentTarget.value = ''
}
private handleDragOver = (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
// Check if the "type" of the datatransfer object includes files. If not, deny drop.
const { types } = event.dataTransfer!
const hasFiles = types.some((type) => type === 'Files')
const { allowNewUpload } = this.uppy.getState()
if (!hasFiles || !allowNewUpload) {
event.dataTransfer!.dropEffect = 'none'
return
}
// Add a small (+) icon on drop
// (and prevent browsers from interpreting this as files being _moved_ into the browser
// https://github.com/transloadit/uppy/issues/1978)
//
event.dataTransfer!.dropEffect = 'copy'
this.setPluginState({ isDraggingOver: true })
this.opts.onDragOver?.(event)
}
private handleDragLeave = (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
this.setPluginState({ isDraggingOver: false })
this.opts.onDragLeave?.(event)
}
private handleDrop = async (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
this.setPluginState({ isDraggingOver: false })
const logDropError = (error: any) => {
this.uppy.log(error, 'error')
}
// Add all dropped files
const files = await getDroppedFiles(event.dataTransfer!, { logDropError })
if (files.length > 0) {
this.uppy.log('[DragDrop] Files dropped')
this.addFiles(files)
}
this.opts.onDrop?.(event)
}
private renderHiddenFileInput() {
const { restrictions } = this.uppy.opts
return (
<input
className="uppy-DragDrop-input"
type="file"
hidden
ref={(ref) => {
this.fileInputRef = ref!
}}
name={this.opts.inputName}
multiple={restrictions.maxNumberOfFiles !== 1}
accept={restrictions.allowedFileTypes?.join(', ')}
onChange={this.onInputChange}
/>
)
}
private static renderArrowSvg() {
return (
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon uppy-DragDrop-arrow"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fillRule="evenodd" />
</svg>
)
}
private renderLabel() {
return (
<div className="uppy-DragDrop-label">
{this.i18nArray('dropHereOr', {
browse: (
<span className="uppy-DragDrop-browse">{this.i18n('browse')}</span>
) as any,
})}
</div>
)
}
private renderNote() {
return <span className="uppy-DragDrop-note">{this.opts.note}</span>
}
render(): ComponentChild {
const dragDropClass = `uppy-u-reset
uppy-DragDrop-container
${this.isDragDropSupported ? 'uppy-DragDrop--isDragDropSupported' : ''}
${this.getPluginState().isDraggingOver ? 'uppy-DragDrop--isDraggingOver' : ''}
`
const dragDropStyle = {
width: this.opts.width,
height: this.opts.height,
}
return (
<button
type="button"
className={dragDropClass}
style={dragDropStyle}
onClick={() => this.fileInputRef.click()}
onDragOver={this.handleDragOver}
onDragLeave={this.handleDragLeave}
onDrop={this.handleDrop}
>
{this.renderHiddenFileInput()}
<div className="uppy-DragDrop-inner">
{DragDrop.renderArrowSvg()}
{this.renderLabel()}
{this.renderNote()}
</div>
</button>
)
}
install(): void {
const { target } = this.opts
this.setPluginState({
isDraggingOver: false,
})
if (target) {
this.mount(target, this)
}
}
uninstall(): void {
this.unmount()
}
}

View file

@ -0,0 +1,2 @@
export type { DragDropOptions } from './DragDrop.js'
export { default } from './DragDrop.js'

View file

@ -0,0 +1,9 @@
export default {
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
dropHereOr: 'Drop here or %{browse}',
// Used as the label for the link that opens the system file selection dialog.
browse: 'browse',
},
}

View file

@ -0,0 +1,67 @@
@use "sass:color";
@use '@uppy/core/src/_utils.scss';
@use '@uppy/core/src/_variables.scss';
.uppy-DragDrop-container {
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
font-family: variables.$font-family-base;
background-color: variables.$white;
border-radius: 7px;
cursor: pointer;
// firefox fix: removes thin dotted outline
&::-moz-focus-inner {
border: 0;
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(variables.$blue, 0.4);
}
}
.uppy-DragDrop-inner {
margin: 0;
padding: 80px 20px;
line-height: 1.4;
text-align: center;
}
.uppy-DragDrop-arrow {
width: 60px;
height: 60px;
margin-bottom: 17px;
fill: color.adjust(variables.$gray-500, $lightness: 30%);
}
.uppy-DragDrop--isDragDropSupported {
border: 2px dashed color.adjust(variables.$gray-500, $lightness: 10%);
}
.uppy-DragDrop--isDraggingOver {
background: variables.$gray-200;
border: 2px dashed variables.$blue;
.uppy-DragDrop-arrow {
fill: variables.$gray-500;
}
}
.uppy-DragDrop-label {
display: block;
margin-bottom: 5px;
font-size: 1.15em;
}
.uppy-DragDrop-browse {
color: variables.$blue;
cursor: pointer;
}
.uppy-DragDrop-note {
color: color.adjust(variables.$gray-500, $lightness: 10%);
font-size: 1em;
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"outDir": "./lib",
"rootDir": "./src"
},
"include": ["./src/**/*.*"],
"exclude": ["./src/**/*.test.ts"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true
},
"include": ["./package.json", "./src/**/*.*"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -0,0 +1,8 @@
{
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["^build", "@uppy/core#build"]
}
}
}

View file

@ -17,13 +17,8 @@ async function getLocalesAndCombinedLocale() {
const locales = await getLocales(`${root}/packages/@uppy/**/lib/locale.js`)
const combinedLocale = {}
for (const [pluginName, locale] of Object.entries(locales)) {
for (const [, locale] of Object.entries(locales)) {
for (const [key, value] of Object.entries(locale.strings)) {
if (key in combinedLocale && value !== combinedLocale[key]) {
throw new Error(
`'${key}' from ${pluginName} already exists in locale pack.`,
)
}
combinedLocale[key] = value
}
}

View file

@ -39,6 +39,7 @@ en_US.strings = {
authenticateWithTitle:
'Please authenticate with %{pluginName} to select files',
back: 'Back',
browse: 'browse',
browseFiles: 'browse files',
browseFolders: 'browse folders',
cancel: 'Cancel',
@ -63,6 +64,7 @@ en_US.strings = {
discardMediaFile: 'Discard Media',
discardRecordedFile: 'Discard recorded file',
done: 'Done',
dropHereOr: 'Drop here or %{browse}',
dropHint: 'Drop your files here',
dropPasteBoth: 'Drop files here, %{browseFiles} or %{browseFolders}',
dropPasteFiles: 'Drop files here or %{browseFiles}',

View file

@ -0,0 +1 @@
tsconfig.*

View file

@ -0,0 +1,215 @@
# @uppy/status-bar
## 4.2.3
### Patch Changes
- c15c6fd: Make each entry in `strings` in locale type optional
## 4.2.2
### Patch Changes
- 1b1a9e3: Define "files" in package.json
- Updated dependencies [1b1a9e3]
- @uppy/utils@6.2.2
- @uppy/core@4.5.2
## 4.2.0
### Minor Changes
- 0c24c5a: Use TypeScript compiler instead of Babel
### Patch Changes
- Updated dependencies [0c24c5a]
- Updated dependencies [0c24c5a]
- @uppy/core@4.5.0
- @uppy/utils@6.2.0
## 4.1.2
Released: 2025-02-25
Included in: Uppy v4.13.3
- @uppy/status-bar: fix aria-hidden warning (Merlijn Vos / #5663)
## 4.1.1
Released: 2025-01-09
Included in: Uppy v4.12.2
- @uppy/status-bar: fix double upload progress (Mikael Finstad / #5587)
## 4.1.0
Released: 2025-01-06
Included in: Uppy v4.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/webdav,@uppy/xhr-upload,@uppy/zoom: Remove "paths" from all tsconfig's (Merlijn Vos / #5572)
## 4.0.6
Released: 2024-12-17
Included in: Uppy v4.9.0
- e2e,@uppy/status-bar,@uppy/utils: Companion stream upload unknown size files (Mikael Finstad / #5489)
## 4.0.5
Released: 2024-12-05
Included in: Uppy v4.8.0
- @uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: cleanup tsconfig (Mikael Finstad / #5520)
## 4.0.4
Released: 2024-10-31
Included in: Uppy v4.6.0
- @uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react-native,@uppy/react,@uppy/redux-dev-tools,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Fix links (Anthony Veaudry / #5492)
## 4.0.3
Released: 2024-08-20
Included in: Uppy v4.2.0
- @uppy/status-bar: show upload button when files are recovered (Merlijn Vos / #5418)
## 4.0.1
Released: 2024-07-30
Included in: Uppy v4.1.0
- @uppy/status-bar: GoldenRetriever + `hideUploadButton=true` (Evgenia Karunus / #5350)
## 4.0.0-beta.9
Released: 2024-06-04
Included in: Uppy v4.0.0-beta.10
- @uppy/status-bar: remove unused component props (Antoine du Hamel / #5211)
- @uppy/status-bar: rename `StatusBar` to `StatusBarUI` (Mikael Finstad / #5200)
## 4.0.0-beta.1
Released: 2024-03-28
Included in: Uppy v4.0.0-beta.1
- @uppy/companion-client,@uppy/provider-views,@uppy/status-bar: fix type imports (Antoine du Hamel / #5038)
- @uppy/status-bar: remove default target (Antoine du Hamel / #4970)
- @uppy/status-bar: refine type of private variables (Antoine du Hamel / #5025)
- @uppy/status-bar: fix `recoveredState` type (Antoine du Hamel / #4996)
## 3.3.3
Released: 2024-05-07
Included in: Uppy v3.25.2
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: Upgrade @transloadit/prettier-bytes (Merlijn Vos / #5150)
## 3.3.1
Released: 2024-03-27
Included in: Uppy v3.24.0
- @uppy/box,@uppy/companion-client,@uppy/provider-views,@uppy/status-bar: fix type imports (Antoine du Hamel / #5038)
- @uppy/status-bar: refine type of private variables (Antoine du Hamel / #5025)
- @uppy/status-bar: fix `recoveredState` type (Antoine du Hamel / #4996)
## 3.2.7
Released: 2024-02-20
Included in: Uppy v3.22.1
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: bump `@transloadit/prettier-bytes` (Antoine du Hamel / #4933)
## 3.2.6
Released: 2024-02-19
Included in: Uppy v3.22.0
- @uppy/status-bar: fix `statusbaroptions` type (antoine du hamel / #4883)
- @uppy/status-bar: refactor to typescript (antoine du hamel / #4839)
## 3.2.4
Released: 2023-08-15
Included in: Uppy v3.14.0
- @uppy/status-bar: e2e: add test for retrying and pausing uploads (Antoine du Hamel / #3599)
## 3.2.3
Released: 2023-07-20
Included in: Uppy v3.13.0
- @uppy/status-bar: fix ETA when status bar is installed during upload (Antoine du Hamel / #4588)
## 3.2.2
Released: 2023-07-13
Included in: Uppy v3.12.0
- @uppy/status-bar: listen to `upload` event instead of button click (Antoine du Hamel / #4563)
## 3.2.1
Released: 2023-07-06
Included in: Uppy v3.11.0
- @uppy/status-bar: remove throttled component (Artur Paikin / #4396)
- @uppy/status-bar: fix ETA when Uppy recovers its state (Antoine du Hamel / #4525)
## 3.2.0
Released: 2023-06-19
Included in: Uppy v3.10.0
- @uppy/companion,@uppy/core,@uppy/dashboard,@uppy/golden-retriever,@uppy/status-bar,@uppy/utils: Migrate all lodash' per-method-packages usage to lodash. (LinusMain / #4274)
- @uppy/status-bar: Filtered ETA (stduhpf / #4458)
## 3.0.1
Released: 2022-09-25
Included in: Uppy v3.1.0
- @uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/companion,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/redux-dev-tools,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092)
## 3.0.0
Released: 2022-08-22
Included in: Uppy v3.0.0
- @uppy/core,@uppy/dashboard,@uppy/status-bar: Style tweaks: use all: initial + other resets (Artur Paikin / #3983)
- Switch to ESM
## 3.0.0-beta.2
Released: 2022-08-03
Included in: Uppy v3.0.0-beta.4
- @uppy/status-bar: rename internal modules (Antoine du Hamel / #3929)
## 2.2.1
Released: 2022-05-30
Included in: Uppy v2.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763)
## 2.2.0
Released: 2022-05-14
Included in: Uppy v2.10.0
- @uppy/status-bar: refactor to ESM (Antoine du Hamel / #3697)
## 2.1.2
Released: 2021-12-07
Included in: Uppy v2.3.0
- @uppy/status-bar: Status bar error state improvements (Merlijn Vos / #3299)
- @uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive,@uppy/image-editor,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/url,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Refactor locale scripts & generate types and docs (Merlijn Vos / #3276)

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Transloadit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,54 @@
# @uppy/status-bar
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
[![npm version](https://img.shields.io/npm/v/@uppy/status-bar.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/status-bar)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/CI/badge.svg)
![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg)
![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg)
The status-bar shows upload progress and speed, ETAs, pre- and post-processing
information, and allows users to control (pause/resume/cancel) the upload. Best
used together with a basic file source plugin, such as
[@uppy/file-input](https://uppy.io/docs/file-input) or
[@uppy/drag-drop](https://uppy.io/docs/drag-drop), or a custom implementation.
Its also included in the [@uppy/dashboard](https://uppy.io/docs/dashboard)
plugin.
Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
a versatile file encoding service.
## Example
```js
import Uppy from '@uppy/core'
import StatusBar from '@uppy/status-bar'
const uppy = new Uppy()
uppy.use(StatusBar, {
target: 'body',
hideUploadButton: false,
showProgressDetails: false,
hideAfterFinish: true,
})
```
## Installation
```bash
$ npm install @uppy/status-bar
```
Alternatively, you can also use this plugin in a pre-built bundle from
Transloadits CDN: Smart CDN. In that case `Uppy` will attach itself to the
global `window.Uppy` object. See the
[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
## Documentation
Documentation for this plugin can be found on the
[Uppy website](https://uppy.io/docs/status-bar).
## License
[The MIT License](./LICENSE).

View file

@ -0,0 +1,64 @@
{
"name": "@uppy/status-bar",
"description": "A progress bar for Uppy, with many bells and whistles.",
"version": "4.2.3",
"license": "MIT",
"main": "lib/index.js",
"style": "dist/style.min.css",
"type": "module",
"sideEffects": [
"*.css"
],
"scripts": {
"build": "tsc --build tsconfig.build.json",
"build:css": "sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",
"typecheck": "tsc --build"
},
"keywords": [
"file uploader",
"uppy",
"uppy-plugin",
"progress bar",
"status bar",
"progress",
"upload",
"eta",
"speed"
],
"homepage": "https://uppy.io",
"bugs": {
"url": "https://github.com/transloadit/uppy/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
},
"files": [
"src",
"lib",
"dist",
"CHANGELOG.md"
],
"exports": {
".": "./lib/index.js",
"./css/style.css": "./dist/style.css",
"./css/style.min.css": "./dist/style.min.css",
"./package.json": "./package.json"
},
"dependencies": {
"@transloadit/prettier-bytes": "^0.3.4",
"@uppy/utils": "workspace:^",
"classnames": "^2.2.6",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
},
"devDependencies": {
"cssnano": "^7.0.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"sass": "^1.89.2",
"typescript": "^5.8.3"
}
}

View file

@ -0,0 +1,546 @@
import prettierBytes from '@transloadit/prettier-bytes'
import type { Body, Meta, State, Uppy } from '@uppy/core'
import type { FileProcessingInfo, I18n } from '@uppy/utils'
import { prettyETA } from '@uppy/utils'
import classNames from 'classnames'
import statusBarStates from './StatusBarStates.js'
const DOT = `\u00B7`
const renderDot = (): string => ` ${DOT} `
interface UploadBtnProps<M extends Meta, B extends Body> {
newFiles: number
isUploadStarted: boolean
recoveredState: State<M, B>['recoveredState']
i18n: I18n
uploadState: string
isSomeGhost: boolean
startUpload: () => void
}
function UploadBtn<M extends Meta, B extends Body>(
props: UploadBtnProps<M, B>,
) {
const {
newFiles,
isUploadStarted,
recoveredState,
i18n,
uploadState,
isSomeGhost,
startUpload,
} = props
const uploadBtnClassNames = classNames(
'uppy-u-reset',
'uppy-c-btn',
'uppy-StatusBar-actionBtn',
'uppy-StatusBar-actionBtn--upload',
{
'uppy-c-btn-primary': uploadState === statusBarStates.STATE_WAITING,
},
{ 'uppy-StatusBar-actionBtn--disabled': isSomeGhost },
)
const uploadBtnText =
newFiles && isUploadStarted && !recoveredState
? i18n('uploadXNewFiles', { smart_count: newFiles })
: i18n('uploadXFiles', { smart_count: newFiles })
return (
<button
type="button"
className={uploadBtnClassNames}
aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
onClick={startUpload}
disabled={isSomeGhost}
data-uppy-super-focusable
>
{uploadBtnText}
</button>
)
}
interface RetryBtnProps<M extends Meta, B extends Body> {
i18n: I18n
uppy: Uppy<M, B>
}
function RetryBtn<M extends Meta, B extends Body>(props: RetryBtnProps<M, B>) {
const { i18n, uppy } = props
return (
<button
type="button"
className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
aria-label={i18n('retryUpload')}
onClick={() =>
uppy.retryAll().catch(() => {
/* Error reported and handled via an event */
})
}
data-uppy-super-focusable
data-cy="retry"
>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
width="8"
height="10"
viewBox="0 0 8 10"
>
<path d="M4 2.408a2.75 2.75 0 1 0 2.75 2.75.626.626 0 0 1 1.25.018v.023a4 4 0 1 1-4-4.041V.25a.25.25 0 0 1 .389-.208l2.299 1.533a.25.25 0 0 1 0 .416l-2.3 1.533A.25.25 0 0 1 4 3.316v-.908z" />
</svg>
{i18n('retry')}
</button>
)
}
interface CancelBtnProps<M extends Meta, B extends Body> {
i18n: I18n
uppy: Uppy<M, B>
}
function CancelBtn<M extends Meta, B extends Body>(
props: CancelBtnProps<M, B>,
) {
const { i18n, uppy } = props
return (
<button
type="button"
className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
title={i18n('cancel')}
aria-label={i18n('cancel')}
onClick={(): void => uppy.cancelAll()}
data-cy="cancel"
data-uppy-super-focusable
>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
width="16"
height="16"
viewBox="0 0 16 16"
>
<g fill="none" fillRule="evenodd">
<circle fill="#888" cx="8" cy="8" r="8" />
<path
fill="#FFF"
d="M9.283 8l2.567 2.567-1.283 1.283L8 9.283 5.433 11.85 4.15 10.567 6.717 8 4.15 5.433 5.433 4.15 8 6.717l2.567-2.567 1.283 1.283z"
/>
</g>
</svg>
</button>
)
}
interface PauseResumeButtonProps<M extends Meta, B extends Body> {
i18n: I18n
uppy: Uppy<M, B>
isAllPaused: boolean
isAllComplete: boolean
resumableUploads: boolean
}
function PauseResumeButton<M extends Meta, B extends Body>(
props: PauseResumeButtonProps<M, B>,
) {
const { isAllPaused, i18n, isAllComplete, resumableUploads, uppy } = props
const title = isAllPaused ? i18n('resume') : i18n('pause')
function togglePauseResume(): void {
if (isAllComplete) return
if (!resumableUploads) {
uppy.cancelAll()
return
}
if (isAllPaused) {
uppy.resumeAll()
return
}
uppy.pauseAll()
}
return (
<button
title={title}
aria-label={title}
className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
type="button"
onClick={togglePauseResume}
data-cy="togglePauseResume"
data-uppy-super-focusable
>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
width="16"
height="16"
viewBox="0 0 16 16"
>
<g fill="none" fillRule="evenodd">
<circle fill="#888" cx="8" cy="8" r="8" />
<path
fill="#FFF"
d={
isAllPaused
? 'M6 4.25L11.5 8 6 11.75z'
: 'M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z'
}
/>
</g>
</svg>
</button>
)
}
interface DoneBtnProps {
i18n: I18n
doneButtonHandler: (() => void) | undefined
}
function DoneBtn(props: DoneBtnProps) {
const { i18n, doneButtonHandler } = props
return (
<button
type="button"
className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--done"
onClick={doneButtonHandler}
data-uppy-super-focusable
>
{i18n('done')}
</button>
)
}
function LoadingSpinner() {
return (
<svg
className="uppy-StatusBar-spinner"
aria-hidden="true"
focusable="false"
width="14"
height="14"
>
<path
d="M13.983 6.547c-.12-2.509-1.64-4.893-3.939-5.936-2.48-1.127-5.488-.656-7.556 1.094C.524 3.367-.398 6.048.162 8.562c.556 2.495 2.46 4.52 4.94 5.183 2.932.784 5.61-.602 7.256-3.015-1.493 1.993-3.745 3.309-6.298 2.868-2.514-.434-4.578-2.349-5.153-4.84a6.226 6.226 0 0 1 2.98-6.778C6.34.586 9.74 1.1 11.373 3.493c.407.596.693 1.282.842 1.988.127.598.073 1.197.161 1.794.078.525.543 1.257 1.15.864.525-.341.49-1.05.456-1.592-.007-.15.02.3 0 0"
fillRule="evenodd"
/>
</svg>
)
}
interface ProgressBarProcessingProps {
progress: FileProcessingInfo
}
function ProgressBarProcessing(props: ProgressBarProcessingProps) {
const { progress } = props
const { value, mode, message } = progress
const dot = `\u00B7`
return (
<div className="uppy-StatusBar-content">
<LoadingSpinner />
{mode === 'determinate' ? `${Math.round(value * 100)}% ${dot} ` : ''}
{message}
</div>
)
}
interface ProgressDetailsProps {
i18n: I18n
numUploads: number
complete: number
totalUploadedSize: number
totalSize: number | null
totalETA: number | null
}
function ProgressDetails(props: ProgressDetailsProps) {
const { numUploads, complete, totalUploadedSize, totalSize, totalETA, i18n } =
props
const ifShowFilesUploadedOfTotal = numUploads > 1
const totalUploadedSizeStr = prettierBytes(totalUploadedSize)
return (
<div className="uppy-StatusBar-statusSecondary">
{ifShowFilesUploadedOfTotal &&
i18n('filesUploadedOfTotal', {
complete,
smart_count: numUploads,
})}
<span className="uppy-StatusBar-additionalInfo">
{/* When should we render this dot?
1. .-additionalInfo is shown (happens only on desktops)
2. AND 'filesUploadedOfTotal' was shown
*/}
{ifShowFilesUploadedOfTotal && renderDot()}
{totalSize != null
? i18n('dataUploadedOfTotal', {
complete: totalUploadedSizeStr,
total: prettierBytes(totalSize),
})
: i18n('dataUploadedOfUnknown', { complete: totalUploadedSizeStr })}
{renderDot()}
{totalETA != null &&
i18n('xTimeLeft', {
time: prettyETA(totalETA),
})}
</span>
</div>
)
}
interface FileUploadCountProps {
i18n: I18n
complete: number
numUploads: number
}
function FileUploadCount(props: FileUploadCountProps) {
const { i18n, complete, numUploads } = props
return (
<div className="uppy-StatusBar-statusSecondary">
{i18n('filesUploadedOfTotal', { complete, smart_count: numUploads })}
</div>
)
}
interface UploadNewlyAddedFilesProps {
i18n: I18n
newFiles: number
startUpload: () => void
}
function UploadNewlyAddedFiles(props: UploadNewlyAddedFilesProps) {
const { i18n, newFiles, startUpload } = props
const uploadBtnClassNames = classNames(
'uppy-u-reset',
'uppy-c-btn',
'uppy-StatusBar-actionBtn',
'uppy-StatusBar-actionBtn--uploadNewlyAdded',
)
return (
<div className="uppy-StatusBar-statusSecondary">
<div className="uppy-StatusBar-statusSecondaryHint">
{i18n('xMoreFilesAdded', { smart_count: newFiles })}
</div>
<button
type="button"
className={uploadBtnClassNames}
aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
onClick={startUpload}
>
{i18n('upload')}
</button>
</div>
)
}
interface ProgressBarUploadingProps {
i18n: I18n
supportsUploadProgress: boolean
totalProgress: number
showProgressDetails: boolean | undefined
isUploadStarted: boolean
isAllComplete: boolean
isAllPaused: boolean
newFiles: number
numUploads: number
complete: number
totalUploadedSize: number
totalSize: number | null
totalETA: number | null
startUpload: () => void
}
function ProgressBarUploading(props: ProgressBarUploadingProps) {
const {
i18n,
supportsUploadProgress,
totalProgress,
showProgressDetails,
isUploadStarted,
isAllComplete,
isAllPaused,
newFiles,
numUploads,
complete,
totalUploadedSize,
totalSize,
totalETA,
startUpload,
} = props
const showUploadNewlyAddedFiles = newFiles && isUploadStarted
if (!isUploadStarted || isAllComplete) {
return null
}
const title = isAllPaused ? i18n('paused') : i18n('uploading')
function renderProgressDetails() {
if (!isAllPaused && !showUploadNewlyAddedFiles && showProgressDetails) {
if (supportsUploadProgress) {
return (
<ProgressDetails
numUploads={numUploads}
complete={complete}
totalUploadedSize={totalUploadedSize}
totalSize={totalSize}
totalETA={totalETA}
i18n={i18n}
/>
)
}
return (
<FileUploadCount
i18n={i18n}
complete={complete}
numUploads={numUploads}
/>
)
}
return null
}
return (
<div className="uppy-StatusBar-content" title={title}>
{!isAllPaused ? <LoadingSpinner /> : null}
<div className="uppy-StatusBar-status">
<div className="uppy-StatusBar-statusPrimary">
{supportsUploadProgress && totalProgress !== 0
? `${title}: ${totalProgress}%`
: title}
</div>
{renderProgressDetails()}
{showUploadNewlyAddedFiles ? (
<UploadNewlyAddedFiles
i18n={i18n}
newFiles={newFiles}
startUpload={startUpload}
/>
) : null}
</div>
</div>
)
}
interface ProgressBarCompleteProps {
i18n: I18n
}
function ProgressBarComplete(props: ProgressBarCompleteProps) {
const { i18n } = props
return (
<div
className="uppy-StatusBar-content"
// biome-ignore lint/a11y/useSemanticElements: ...
role="status"
title={i18n('complete')}
>
<div className="uppy-StatusBar-status">
<div className="uppy-StatusBar-statusPrimary">
<svg
aria-hidden="true"
focusable="false"
className="uppy-StatusBar-statusIndicator uppy-c-icon"
width="15"
height="11"
viewBox="0 0 15 11"
>
<path d="M.414 5.843L1.627 4.63l3.472 3.472L13.202 0l1.212 1.213L5.1 10.528z" />
</svg>
{i18n('complete')}
</div>
</div>
</div>
)
}
interface ProgressBarErrorProps {
i18n: I18n
error: any
complete: number
numUploads: number
}
function ProgressBarError(props: ProgressBarErrorProps) {
const { error, i18n, complete, numUploads } = props
function displayErrorAlert(): void {
const errorMessage = `${i18n('uploadFailed')} \n\n ${error}`
alert(errorMessage) // TODO: move to custom alert implementation
}
return (
<div className="uppy-StatusBar-content" title={i18n('uploadFailed')}>
<svg
aria-hidden="true"
focusable="false"
className="uppy-StatusBar-statusIndicator uppy-c-icon"
width="11"
height="11"
viewBox="0 0 11 11"
>
<path d="M4.278 5.5L0 1.222 1.222 0 5.5 4.278 9.778 0 11 1.222 6.722 5.5 11 9.778 9.778 11 5.5 6.722 1.222 11 0 9.778z" />
</svg>
<div className="uppy-StatusBar-status">
<div className="uppy-StatusBar-statusPrimary">
{i18n('uploadFailed')}
<button
className="uppy-u-reset uppy-StatusBar-details"
aria-label={i18n('showErrorDetails')}
data-microtip-position="top-right"
data-microtip-size="medium"
onClick={displayErrorAlert}
type="button"
>
?
</button>
</div>
<FileUploadCount
i18n={i18n}
complete={complete}
numUploads={numUploads}
/>
</div>
</div>
)
}
export {
UploadBtn,
RetryBtn,
CancelBtn,
PauseResumeButton,
DoneBtn,
LoadingSpinner,
ProgressDetails,
ProgressBarProcessing,
ProgressBarError,
ProgressBarUploading,
ProgressBarComplete,
}

View file

@ -0,0 +1,301 @@
import type {
Body,
DefinePluginOpts,
Meta,
State,
Uppy,
UppyFile,
} from '@uppy/core'
import { UIPlugin } from '@uppy/core'
import { emaFilter, getTextDirection } from '@uppy/utils'
import type { ComponentChild } from 'preact'
import packageJson from '../package.json' with { type: 'json' }
import locale from './locale.js'
import type { StatusBarOptions } from './StatusBarOptions.js'
import statusBarStates from './StatusBarStates.js'
import StatusBarUI, { type StatusBarUIProps } from './StatusBarUI.js'
const speedFilterHalfLife = 2000
const ETAFilterHalfLife = 2000
function getUploadingState(
error: unknown,
isAllComplete: boolean,
recoveredState: any,
files: Record<string, UppyFile<any, any>>,
): StatusBarUIProps<any, any>['uploadState'] {
if (error) {
return statusBarStates.STATE_ERROR
}
if (isAllComplete) {
return statusBarStates.STATE_COMPLETE
}
if (recoveredState) {
return statusBarStates.STATE_WAITING
}
let state: StatusBarUIProps<any, any>['uploadState'] =
statusBarStates.STATE_WAITING
const fileIDs = Object.keys(files)
for (let i = 0; i < fileIDs.length; i++) {
const { progress } = files[fileIDs[i]]
// If ANY files are being uploaded right now, show the uploading state.
if (progress.uploadStarted && !progress.uploadComplete) {
return statusBarStates.STATE_UPLOADING
}
// If files are being preprocessed AND postprocessed at this time, we show the
// preprocess state. If any files are being uploaded we show uploading.
if (progress.preprocess) {
state = statusBarStates.STATE_PREPROCESSING
}
// If NO files are being preprocessed or uploaded right now, but some files are
// being postprocessed, show the postprocess state.
if (progress.postprocess && state !== statusBarStates.STATE_PREPROCESSING) {
state = statusBarStates.STATE_POSTPROCESSING
}
}
return state
}
const defaultOptions = {
hideUploadButton: false,
hideRetryButton: false,
hidePauseResumeButton: false,
hideCancelButton: false,
showProgressDetails: false,
hideAfterFinish: true,
doneButtonHandler: null,
} satisfies StatusBarOptions
/**
* StatusBar: renders a status bar with upload/pause/resume/cancel/retry buttons,
* progress percentage and time remaining.
*/
export default class StatusBar<M extends Meta, B extends Body> extends UIPlugin<
DefinePluginOpts<StatusBarOptions, keyof typeof defaultOptions>,
M,
B
> {
static VERSION = packageJson.version
#lastUpdateTime!: ReturnType<typeof performance.now>
#previousUploadedBytes!: number | null
#previousSpeed!: number | null
#previousETA!: number | null
constructor(uppy: Uppy<M, B>, opts?: StatusBarOptions) {
super(uppy, { ...defaultOptions, ...opts })
this.id = this.opts.id || 'StatusBar'
this.title = 'StatusBar'
this.type = 'progressindicator'
this.defaultLocale = locale
this.i18nInit()
this.render = this.render.bind(this)
this.install = this.install.bind(this)
}
#computeSmoothETA(totalBytes: {
uploaded: number
total: number | null // null means indeterminate
}) {
if (totalBytes.total == null || totalBytes.total === 0) {
return null
}
const remaining = totalBytes.total - totalBytes.uploaded
if (remaining <= 0) {
return null
}
// When state is restored, lastUpdateTime is still nullish at this point.
this.#lastUpdateTime ??= performance.now()
const dt = performance.now() - this.#lastUpdateTime
if (dt === 0) {
return Math.round((this.#previousETA ?? 0) / 100) / 10
}
const uploadedBytesSinceLastTick =
totalBytes.uploaded - this.#previousUploadedBytes!
this.#previousUploadedBytes = totalBytes.uploaded
// uploadedBytesSinceLastTick can be negative in some cases (packet loss?)
// in which case, we wait for next tick to update ETA.
if (uploadedBytesSinceLastTick <= 0) {
return Math.round((this.#previousETA ?? 0) / 100) / 10
}
const currentSpeed = uploadedBytesSinceLastTick / dt
const filteredSpeed =
this.#previousSpeed == null
? currentSpeed
: emaFilter(currentSpeed, this.#previousSpeed, speedFilterHalfLife, dt)
this.#previousSpeed = filteredSpeed
const instantETA = remaining / filteredSpeed
const updatedPreviousETA = Math.max(this.#previousETA! - dt, 0)
const filteredETA =
this.#previousETA == null
? instantETA
: emaFilter(instantETA, updatedPreviousETA, ETAFilterHalfLife, dt)
this.#previousETA = filteredETA
this.#lastUpdateTime = performance.now()
return Math.round(filteredETA / 100) / 10
}
startUpload = (): ReturnType<Uppy<M, B>['upload']> => {
return this.uppy.upload().catch((() => {
// Error logged in Core
}) as () => undefined)
}
render(state: State<M, B>): ComponentChild {
const {
capabilities,
files,
allowNewUpload,
totalProgress,
error,
recoveredState,
} = state
const {
newFiles,
startedFiles,
completeFiles,
isUploadStarted,
isAllComplete,
isAllPaused,
isUploadInProgress,
isSomeGhost,
} = this.uppy.getObjectOfFilesPerState()
// If some state was recovered, we want to show Upload button/counter
// for all the files, because in this case its not an Upload button,
// but “Confirm Restore Button”
const newFilesOrRecovered = recoveredState ? Object.values(files) : newFiles
const resumableUploads = !!capabilities.resumableUploads
const supportsUploadProgress = capabilities.uploadProgress !== false
let totalSize: number | null = null
let totalUploadedSize = 0
// Only if all files have a known size, does it make sense to display a total size
if (
startedFiles.every(
(f) => f.progress.bytesTotal != null && f.progress.bytesTotal !== 0,
)
) {
totalSize = 0
startedFiles.forEach((file) => {
totalSize! += file.progress.bytesTotal || 0
totalUploadedSize += file.progress.bytesUploaded || 0
})
} else {
// however uploaded size we will always have
startedFiles.forEach((file) => {
totalUploadedSize += file.progress.bytesUploaded || 0
})
}
const totalETA = this.#computeSmoothETA({
uploaded: totalUploadedSize,
total: totalSize,
})
return StatusBarUI({
error,
uploadState: getUploadingState(
error,
isAllComplete,
recoveredState,
state.files || {},
),
allowNewUpload,
totalProgress,
totalSize,
totalUploadedSize,
isAllComplete: false,
isAllPaused,
isUploadStarted,
isUploadInProgress,
isSomeGhost,
recoveredState,
complete: completeFiles.length,
newFiles: newFilesOrRecovered.length,
numUploads: startedFiles.length,
totalETA,
files,
i18n: this.i18n,
uppy: this.uppy,
startUpload: this.startUpload,
doneButtonHandler: this.opts.doneButtonHandler,
resumableUploads,
supportsUploadProgress,
showProgressDetails: this.opts.showProgressDetails,
hideUploadButton: this.opts.hideUploadButton,
hideRetryButton: this.opts.hideRetryButton,
hidePauseResumeButton: this.opts.hidePauseResumeButton,
hideCancelButton: this.opts.hideCancelButton,
hideAfterFinish: this.opts.hideAfterFinish,
})
}
onMount(): void {
// Set the text direction if the page has not defined one.
const element = this.el!
const direction = getTextDirection(element)
if (!direction) {
element.dir = 'ltr'
}
}
#onUploadStart = (): void => {
const { recoveredState } = this.uppy.getState()
this.#previousSpeed = null
this.#previousETA = null
if (recoveredState) {
this.#previousUploadedBytes = Object.values(recoveredState.files).reduce(
(pv, { progress }) => pv + (progress.bytesUploaded as number),
0,
)
// We don't set `#lastUpdateTime` at this point because the upload won't
// actually resume until the user asks for it.
this.uppy.emit('restore-confirmed')
return
}
this.#lastUpdateTime = performance.now()
this.#previousUploadedBytes = 0
}
install(): void {
const { target } = this.opts
if (target) {
this.mount(target, this)
}
this.uppy.on('upload', this.#onUploadStart)
// To cover the use case where the status bar is installed while the upload
// has started, we set `lastUpdateTime` right away.
this.#lastUpdateTime = performance.now()
this.#previousUploadedBytes = this.uppy
.getFiles()
.reduce((pv, file) => pv + (file.progress.bytesUploaded as number), 0)
}
uninstall(): void {
this.unmount()
this.uppy.off('upload', this.#onUploadStart)
}
}

View file

@ -0,0 +1,14 @@
import type { UIPluginOptions } from '@uppy/core'
import type { LocaleStrings } from '@uppy/utils'
import type StatusBarLocale from './locale.js'
export interface StatusBarOptions extends UIPluginOptions {
showProgressDetails?: boolean
hideUploadButton?: boolean
hideAfterFinish?: boolean
hideRetryButton?: boolean
hidePauseResumeButton?: boolean
hideCancelButton?: boolean
doneButtonHandler?: (() => void) | null
locale?: LocaleStrings<typeof StatusBarLocale>
}

View file

@ -0,0 +1,8 @@
export default {
STATE_ERROR: 'error' as const,
STATE_WAITING: 'waiting' as const,
STATE_PREPROCESSING: 'preprocessing' as const,
STATE_UPLOADING: 'uploading' as const,
STATE_POSTPROCESSING: 'postprocessing' as const,
STATE_COMPLETE: 'complete' as const,
}

View file

@ -0,0 +1,275 @@
import type { Body, Meta, State, Uppy, UppyFile } from '@uppy/core'
import type { I18n } from '@uppy/utils'
import classNames from 'classnames'
import {
CancelBtn,
DoneBtn,
PauseResumeButton,
ProgressBarComplete,
ProgressBarError,
ProgressBarProcessing,
ProgressBarUploading,
RetryBtn,
UploadBtn,
} from './Components.js'
import calculateProcessingProgress from './calculateProcessingProgress.js'
import statusBarStates from './StatusBarStates.js'
const {
STATE_ERROR,
STATE_WAITING,
STATE_PREPROCESSING,
STATE_UPLOADING,
STATE_POSTPROCESSING,
STATE_COMPLETE,
} = statusBarStates
export interface StatusBarUIProps<M extends Meta, B extends Body> {
newFiles: number
allowNewUpload: boolean
isUploadInProgress: boolean
isAllPaused: boolean
resumableUploads: boolean
error: any
hideUploadButton?: boolean
hidePauseResumeButton?: boolean
hideCancelButton?: boolean
hideRetryButton?: boolean
recoveredState: State<M, B>['recoveredState']
uploadState: (typeof statusBarStates)[keyof typeof statusBarStates]
totalProgress: number
files: Record<string, UppyFile<M, B>>
supportsUploadProgress: boolean
hideAfterFinish?: boolean
isSomeGhost: boolean
doneButtonHandler?: (() => void) | null
isUploadStarted: boolean
i18n: I18n
startUpload: () => void
uppy: Uppy<M, B>
isAllComplete: boolean
showProgressDetails?: boolean
numUploads: number
complete: number
totalSize: number | null
totalETA: number | null
totalUploadedSize: number
}
export default function StatusBarUI<M extends Meta, B extends Body>({
newFiles,
allowNewUpload,
isUploadInProgress,
isAllPaused,
resumableUploads,
error,
hideUploadButton = undefined,
hidePauseResumeButton = false,
hideCancelButton = false,
hideRetryButton = false,
recoveredState,
uploadState,
totalProgress,
files,
supportsUploadProgress,
hideAfterFinish = false,
isSomeGhost,
doneButtonHandler = undefined,
isUploadStarted,
i18n,
startUpload,
uppy,
isAllComplete,
showProgressDetails = undefined,
numUploads,
complete,
totalSize,
totalETA,
totalUploadedSize,
}: StatusBarUIProps<M, B>) {
function getProgressValue(): number | null {
switch (uploadState) {
case STATE_POSTPROCESSING:
case STATE_PREPROCESSING: {
const progress = calculateProcessingProgress(files)
if (progress.mode === 'determinate') {
return progress.value * 100
}
return totalProgress
}
case STATE_ERROR: {
return null
}
case STATE_UPLOADING: {
if (!supportsUploadProgress) {
return null
}
return totalProgress
}
default:
return totalProgress
}
}
function getIsIndeterminate(): boolean {
switch (uploadState) {
case STATE_POSTPROCESSING:
case STATE_PREPROCESSING: {
const { mode } = calculateProcessingProgress(files)
return mode === 'indeterminate'
}
case STATE_UPLOADING: {
if (!supportsUploadProgress) {
return true
}
return false
}
default:
return false
}
}
const progressValue = getProgressValue()
const width = progressValue ?? 100
const showUploadBtn =
!error &&
newFiles &&
((!isUploadInProgress && !isAllPaused) || recoveredState) &&
allowNewUpload &&
!hideUploadButton
const showCancelBtn =
!hideCancelButton &&
uploadState !== STATE_WAITING &&
uploadState !== STATE_COMPLETE
const showPauseResumeBtn =
resumableUploads &&
!hidePauseResumeButton &&
uploadState === STATE_UPLOADING
const showRetryBtn = error && !isAllComplete && !hideRetryButton
const showDoneBtn = doneButtonHandler && uploadState === STATE_COMPLETE
const progressClassNames = classNames('uppy-StatusBar-progress', {
'is-indeterminate': getIsIndeterminate(),
})
const statusBarClassNames = classNames(
'uppy-StatusBar',
`is-${uploadState}`,
{ 'has-ghosts': isSomeGhost },
)
const progressBarStateEl = (() => {
switch (uploadState) {
case STATE_PREPROCESSING:
case STATE_POSTPROCESSING:
return (
<ProgressBarProcessing
progress={calculateProcessingProgress(files)}
/>
)
case STATE_COMPLETE:
return <ProgressBarComplete i18n={i18n} />
case STATE_ERROR:
return (
<ProgressBarError
error={error}
i18n={i18n}
numUploads={numUploads}
complete={complete}
/>
)
case STATE_UPLOADING:
return (
<ProgressBarUploading
i18n={i18n}
supportsUploadProgress={supportsUploadProgress}
totalProgress={totalProgress}
showProgressDetails={showProgressDetails}
isUploadStarted={isUploadStarted}
isAllComplete={isAllComplete}
isAllPaused={isAllPaused}
newFiles={newFiles}
numUploads={numUploads}
complete={complete}
totalUploadedSize={totalUploadedSize}
totalSize={totalSize}
totalETA={totalETA}
startUpload={startUpload}
/>
)
default:
return null
}
})()
const atLeastOneAction =
showUploadBtn ||
showRetryBtn ||
showPauseResumeBtn ||
showCancelBtn ||
showDoneBtn
const thereIsNothingInside = !atLeastOneAction && !progressBarStateEl
const isHidden =
thereIsNothingInside || (uploadState === STATE_COMPLETE && hideAfterFinish)
if (isHidden) {
return null
}
return (
<div className={statusBarClassNames}>
<div
className={progressClassNames}
style={{ width: `${width}%` }}
role="progressbar"
aria-label={`${width}%`}
aria-valuetext={`${width}%`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progressValue!}
/>
{progressBarStateEl}
<div className="uppy-StatusBar-actions">
{showUploadBtn ? (
<UploadBtn
newFiles={newFiles}
isUploadStarted={isUploadStarted}
recoveredState={recoveredState}
i18n={i18n}
isSomeGhost={isSomeGhost}
startUpload={startUpload}
uploadState={uploadState}
/>
) : null}
{showRetryBtn ? <RetryBtn i18n={i18n} uppy={uppy} /> : null}
{showPauseResumeBtn ? (
<PauseResumeButton
isAllPaused={isAllPaused}
i18n={i18n}
isAllComplete={isAllComplete}
resumableUploads={resumableUploads}
uppy={uppy}
/>
) : null}
{showCancelBtn ? <CancelBtn i18n={i18n} uppy={uppy} /> : null}
{showDoneBtn ? (
<DoneBtn i18n={i18n} doneButtonHandler={doneButtonHandler} />
) : null}
</div>
</div>
)
}

View file

@ -0,0 +1,30 @@
import type { FileProcessingInfo, UppyFile } from '@uppy/utils'
export default function calculateProcessingProgress(
files: Record<string, UppyFile<any, any>>,
): FileProcessingInfo {
const values: number[] = []
let mode: FileProcessingInfo['mode'] = 'indeterminate'
let message: FileProcessingInfo['message']
for (const { progress } of Object.values(files)) {
const { preprocess, postprocess } = progress
// In the future we should probably do this differently. For now we'll take the
// mode and message from the first file…
if (message == null && (preprocess || postprocess)) {
;({ mode, message } = preprocess || postprocess!)
}
if (preprocess?.mode === 'determinate') values.push(preprocess.value)
if (postprocess?.mode === 'determinate') values.push(postprocess.value)
}
const value = values.reduce((total, progressValue) => {
return total + progressValue / values.length
}, 0)
return {
mode,
message,
value,
} as FileProcessingInfo
}

View file

@ -0,0 +1,2 @@
export { default } from './StatusBar.js'
export type { StatusBarOptions } from './StatusBarOptions.js'

View file

@ -0,0 +1,50 @@
export default {
strings: {
// Shown in the status bar while files are being uploaded.
uploading: 'Uploading',
// Shown in the status bar once all files have been uploaded.
complete: 'Complete',
// Shown in the status bar if an upload failed.
uploadFailed: 'Upload failed',
// Shown in the status bar while the upload is paused.
paused: 'Paused',
// Used as the label for the button that retries an upload.
retry: 'Retry',
// Used as the label for the button that cancels an upload.
cancel: 'Cancel',
// Used as the label for the button that pauses an upload.
pause: 'Pause',
// Used as the label for the button that resumes an upload.
resume: 'Resume',
// Used as the label for the button that resets the upload state after an upload
done: 'Done',
// When `showProgressDetails` is set, shows the number of files that have been fully uploaded so far.
filesUploadedOfTotal: {
0: '%{complete} of %{smart_count} file uploaded',
1: '%{complete} of %{smart_count} files uploaded',
},
// When `showProgressDetails` is set, shows the amount of bytes that have been uploaded so far.
dataUploadedOfTotal: '%{complete} of %{total}',
dataUploadedOfUnknown: '%{complete} of unknown',
// When `showProgressDetails` is set, shows an estimation of how long the upload will take to complete.
xTimeLeft: '%{time} left',
// Used as the label for the button that starts an upload.
uploadXFiles: {
0: 'Upload %{smart_count} file',
1: 'Upload %{smart_count} files',
},
// Used as the label for the button that starts an upload, if another upload has been started in the past
// and new files were added later.
uploadXNewFiles: {
0: 'Upload +%{smart_count} file',
1: 'Upload +%{smart_count} files',
},
upload: 'Upload',
retryUpload: 'Retry upload',
xMoreFilesAdded: {
0: '%{smart_count} more file added',
1: '%{smart_count} more files added',
},
showErrorDetails: 'Show error details',
},
}

View file

@ -0,0 +1,463 @@
@use "sass:color";
@use '@uppy/core/src/_utils.scss';
@use '@uppy/core/src/_variables.scss';
@use '@uppy/utils/src/microtip.scss';
.uppy-StatusBar {
position: relative;
z-index: variables.$zIndex-2;
display: flex;
height: 46px;
color: variables.$white;
font-weight: 400;
font-size: 12px;
line-height: 40px;
background-color: variables.$white;
transition: height 0.2s;
[data-uppy-theme='dark'] & {
background-color: variables.$gray-900;
}
}
.uppy-StatusBar::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: variables.$gray-200;
content: '';
[data-uppy-theme='dark'] & {
background-color: variables.$gray-600;
}
}
.uppy-StatusBar[aria-hidden='true'] {
height: 0;
overflow-y: hidden;
}
.uppy-StatusBar.is-complete .uppy-StatusBar-progress {
background-color: variables.$green;
}
.uppy-StatusBar.is-error .uppy-StatusBar-progress {
background-color: variables.$red;
}
.uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator {
color: variables.$green;
}
.uppy-StatusBar.is-error .uppy-StatusBar-statusIndicator {
color: variables.$red;
}
.uppy-StatusBar:not([aria-hidden='true']).is-waiting {
height: 65px;
background-color: variables.$white;
border-top: 1px solid variables.$gray-200;
[data-uppy-theme='dark'] & {
background-color: variables.$gray-900;
border-top: 1px solid variables.$gray-800;
}
}
.uppy-StatusBar-progress {
position: absolute;
z-index: variables.$zIndex-2;
height: 2px;
background-color: variables.$blue;
transition:
background-color,
width 0.3s ease-out;
&.is-indeterminate {
$stripe-color: rgba(0, 0, 0, 0.3);
background-image: linear-gradient(
45deg,
$stripe-color 25%,
transparent 25%,
transparent 50%,
$stripe-color 50%,
$stripe-color 75%,
transparent 75%,
transparent
);
background-size: 64px 64px;
animation: uppy-StatusBar-ProgressStripes 1s linear infinite;
}
}
@keyframes uppy-StatusBar-ProgressStripes {
from {
background-position: 0 0;
}
to {
background-position: 64px 0;
}
}
.uppy-StatusBar.is-preprocessing .uppy-StatusBar-progress,
.uppy-StatusBar.is-postprocessing .uppy-StatusBar-progress {
background-color: variables.$orange;
}
.uppy-StatusBar.is-waiting .uppy-StatusBar-progress {
display: none;
}
.uppy-StatusBar-content {
position: relative;
z-index: variables.$zIndex-3;
display: flex;
align-items: center;
height: 100%;
color: variables.$gray-800;
white-space: nowrap;
text-overflow: ellipsis;
padding-inline-start: 10px;
.uppy-size--md & {
padding-inline-start: 15px;
}
[data-uppy-theme='dark'] & {
color: variables.$gray-200;
}
}
.uppy-StatusBar-status {
display: flex;
flex-direction: column;
justify-content: center;
font-weight: normal;
line-height: 1.4;
padding-inline-end: 0.3em;
}
.uppy-StatusBar-statusPrimary {
font-weight: 500;
line-height: 1;
display: flex;
button.uppy-StatusBar-details {
margin-left: 5px;
}
[data-uppy-theme='dark'] & {
color: variables.$gray-200;
}
}
.uppy-StatusBar-statusSecondary {
display: inline-block;
margin-top: 1px;
color: variables.$gray-600;
font-size: 11px;
line-height: 1.2;
white-space: nowrap;
[data-uppy-theme='dark'] & {
color: variables.$gray-400;
}
}
.uppy-StatusBar-statusSecondaryHint {
display: inline-block;
line-height: 1;
vertical-align: middle;
margin-inline-end: 5px;
.uppy-size--md & {
margin-inline-end: 8px;
}
}
.uppy-StatusBar-statusIndicator {
position: relative;
top: 1px;
color: variables.$gray-700;
margin-inline-end: 7px;
svg {
vertical-align: text-bottom;
}
}
.uppy-StatusBar-actions {
position: absolute;
top: 0;
bottom: 0;
z-index: variables.$zIndex-4;
display: flex;
align-items: center;
inset-inline-end: 10px;
}
.uppy-StatusBar.is-waiting .uppy-StatusBar-actions {
position: static;
width: 100%;
height: 100%;
padding: 0 15px;
background-color: variables.$gray-50;
[data-uppy-theme='dark'] & {
background-color: variables.$gray-900;
}
}
.uppy-StatusBar:not([aria-hidden='true']).is-waiting.has-ghosts {
flex-direction: column;
height: 90px;
.uppy-size--md & {
flex-direction: row;
height: 65px;
}
.uppy-StatusBar-actions {
flex-direction: column;
justify-content: center;
.uppy-size--md & {
flex-direction: row;
justify-content: initial;
}
}
}
.uppy-StatusBar-actionCircleBtn {
@include utils.blue-border-focus;
margin: 3px;
line-height: 1;
cursor: pointer;
opacity: 0.9;
[data-uppy-theme='dark'] & {
@include utils.blue-border-focus--dark;
}
&:hover {
opacity: 1;
}
&:focus {
border-radius: 50%;
}
}
.uppy-StatusBar-actionCircleBtn svg {
vertical-align: bottom;
}
.uppy-StatusBar-actionBtn {
display: inline-block;
color: variables.$blue;
font-size: 10px;
line-height: inherit;
vertical-align: middle;
.uppy-size--md & {
font-size: 11px;
}
}
.uppy-StatusBar-actionBtn--disabled {
opacity: 0.4;
[data-uppy-theme='dark'] & {
opacity: 0.7;
}
}
.uppy-StatusBar-actionBtn--retry {
@include utils.blue-border-focus();
position: relative;
height: 16px;
padding: 1px 6px 3px 18px;
color: #fff;
line-height: 1;
background-color: variables.$pomegranate;
border-radius: 8px;
margin-inline-end: 6px;
[data-uppy-theme='dark'] & {
@include utils.blue-border-focus--dark;
}
&:hover {
background-color: color.adjust(variables.$pomegranate, $lightness: -8%);
}
svg {
position: absolute;
top: 3px;
inset-inline-start: 6px;
}
}
.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload {
width: 100%;
padding: 15px 10px;
color: variables.$white;
font-size: 14px;
line-height: 1;
background-color: variables.$green;
&:hover {
background-color: color.adjust(variables.$green, $lightness: -5%);
}
[data-uppy-theme='dark'] & {
background-color: variables.$darkgreen;
}
[data-uppy-theme='dark'] &:hover {
background-color: color.adjust(variables.$darkgreen, $lightness: -5%);
}
}
.uppy-size--md .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload {
width: auto;
padding: 13px 22px;
}
.uppy-StatusBar.is-waiting
.uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover {
background-color: variables.$green;
cursor: not-allowed;
}
[data-uppy-theme='dark']
.uppy-StatusBar.is-waiting
.uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover {
background-color: variables.$darkgreen;
}
.uppy-StatusBar:not(.is-waiting) .uppy-StatusBar-actionBtn--upload {
color: variables.$blue;
background-color: transparent;
}
.uppy-StatusBar-actionBtn--uploadNewlyAdded {
// for focus
@include utils.blue-border-focus;
padding-inline-end: 3px;
padding-inline-start: 3px;
padding-bottom: 1px;
border-radius: 3px;
[data-uppy-theme='dark'] & {
@include utils.blue-border-focus--dark;
}
.uppy-StatusBar.is-preprocessing &,
.uppy-StatusBar.is-postprocessing & {
display: none;
}
}
.uppy-StatusBar-actionBtn--done {
@include utils.highlight-focus;
padding: 7px 8px;
line-height: 1;
border-radius: 3px;
[data-uppy-theme='dark'] & {
color: variables.$highlight--dark;
}
}
.uppy-size--md .uppy-StatusBar-actionBtn--done {
font-size: 14px;
}
.uppy-StatusBar-serviceMsg {
padding-left: 10px;
color: variables.$black;
font-size: 11px;
line-height: 1.1;
.uppy-size--md & {
padding-left: 15px;
font-size: 14px;
}
[data-uppy-theme='dark'] & {
color: variables.$gray-200;
}
}
.uppy-StatusBar-serviceMsg-ghostsIcon {
position: relative;
top: 2px;
left: 6px;
width: 10px;
vertical-align: text-bottom;
opacity: 0.5;
.uppy-size--md & {
top: 1px;
left: 10px;
width: 15px;
}
}
.uppy-StatusBar-details {
position: relative;
top: 0;
display: inline-block;
width: 13px;
height: 13px;
color: variables.$white;
font-weight: 600;
font-size: 10px;
line-height: 12px;
text-align: center;
vertical-align: middle;
background-color: variables.$gray-500;
border-radius: 50%;
cursor: help;
appearance: none;
inset-inline-start: 2px;
}
.uppy-StatusBar-details::after {
line-height: 1.3;
word-wrap: break-word;
}
.uppy-StatusBar-spinner {
animation-name: uppy-StatusBar-spinnerAnimation;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
fill: variables.$blue;
margin-inline-end: 10px;
}
.uppy-StatusBar.is-preprocessing .uppy-StatusBar-spinner,
.uppy-StatusBar.is-postprocessing .uppy-StatusBar-spinner {
fill: variables.$orange;
}
@keyframes uppy-StatusBar-spinnerAnimation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"outDir": "./lib",
"rootDir": "./src"
},
"include": ["./src/**/*.*"],
"exclude": ["./src/**/*.test.ts"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true
},
"include": ["./package.json", "./src/**/*.*"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -0,0 +1,8 @@
{
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["^build", "@uppy/core#build"]
}
}
}

View file

@ -44,6 +44,7 @@
"@uppy/compressor": "workspace:^",
"@uppy/core": "workspace:^",
"@uppy/dashboard": "workspace:^",
"@uppy/drag-drop": "workspace:^",
"@uppy/drop-target": "workspace:^",
"@uppy/dropbox": "workspace:^",
"@uppy/facebook": "workspace:^",
@ -59,6 +60,7 @@
"@uppy/provider-views": "workspace:^",
"@uppy/remote-sources": "workspace:^",
"@uppy/screen-capture": "workspace:^",
"@uppy/status-bar": "workspace:^",
"@uppy/store-default": "workspace:^",
"@uppy/thumbnail-generator": "workspace:^",
"@uppy/transloadit": "workspace:^",

View file

@ -25,6 +25,7 @@ export { default as Box } from '@uppy/box'
export { default as Compressor } from '@uppy/compressor'
// UI plugins
export { default as Dashboard } from '@uppy/dashboard'
export { default as DragDrop } from '@uppy/drag-drop'
export { default as DropTarget } from '@uppy/drop-target'
export { default as Dropbox } from '@uppy/dropbox'
export { default as Facebook } from '@uppy/facebook'
@ -38,6 +39,7 @@ export { default as Instagram } from '@uppy/instagram'
export { default as OneDrive } from '@uppy/onedrive'
export { default as RemoteSources } from '@uppy/remote-sources'
export { default as ScreenCapture } from '@uppy/screen-capture'
export { default as StatusBar } from '@uppy/status-bar'
// Stores
export { default as DefaultStore } from '@uppy/store-default'
export { default as ThumbnailGenerator } from '@uppy/thumbnail-generator'

View file

@ -9,6 +9,7 @@ export type { CompressorOptions } from '@uppy/compressor'
export type { UIPluginOptions, UppyOptions } from '@uppy/core'
// UI plugins
export type { DashboardOptions } from '@uppy/dashboard'
export type { DragDropOptions } from '@uppy/drag-drop'
export type { DropTargetOptions } from '@uppy/drop-target'
export type { DropboxOptions } from '@uppy/dropbox'
export type { FacebookOptions } from '@uppy/facebook'
@ -20,6 +21,7 @@ export type { InstagramOptions } from '@uppy/instagram'
export type { OneDriveOptions } from '@uppy/onedrive'
export type { RemoteSourcesOptions } from '@uppy/remote-sources'
export type { ScreenCaptureOptions } from '@uppy/screen-capture'
export type { StatusBarOptions } from '@uppy/status-bar'
export type { ThumbnailGeneratorOptions } from '@uppy/thumbnail-generator'
export type { TransloaditOptions } from '@uppy/transloadit'
export type { TusOptions } from '@uppy/tus'

View file

@ -9296,6 +9296,22 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/drag-drop@workspace:^, @uppy/drag-drop@workspace:packages/@uppy/drag-drop":
version: 0.0.0-use.local
resolution: "@uppy/drag-drop@workspace:packages/@uppy/drag-drop"
dependencies:
"@uppy/utils": "workspace:^"
cssnano: "npm:^7.0.7"
postcss: "npm:^8.5.6"
postcss-cli: "npm:^11.0.1"
preact: "npm:^10.5.13"
sass: "npm:^1.89.2"
typescript: "npm:^5.8.3"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/drop-target@workspace:*, @uppy/drop-target@workspace:^, @uppy/drop-target@workspace:packages/@uppy/drop-target":
version: 0.0.0-use.local
resolution: "@uppy/drop-target@workspace:packages/@uppy/drop-target"
@ -9569,6 +9585,24 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/status-bar@workspace:^, @uppy/status-bar@workspace:packages/@uppy/status-bar":
version: 0.0.0-use.local
resolution: "@uppy/status-bar@workspace:packages/@uppy/status-bar"
dependencies:
"@transloadit/prettier-bytes": "npm:^0.3.4"
"@uppy/utils": "workspace:^"
classnames: "npm:^2.2.6"
cssnano: "npm:^7.0.7"
postcss: "npm:^8.5.6"
postcss-cli: "npm:^11.0.1"
preact: "npm:^10.5.13"
sass: "npm:^1.89.2"
typescript: "npm:^5.8.3"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/store-default@workspace:^, @uppy/store-default@workspace:packages/@uppy/store-default":
version: 0.0.0-use.local
resolution: "@uppy/store-default@workspace:packages/@uppy/store-default"
@ -23851,6 +23885,7 @@ __metadata:
"@uppy/compressor": "workspace:^"
"@uppy/core": "workspace:^"
"@uppy/dashboard": "workspace:^"
"@uppy/drag-drop": "workspace:^"
"@uppy/drop-target": "workspace:^"
"@uppy/dropbox": "workspace:^"
"@uppy/facebook": "workspace:^"
@ -23866,6 +23901,7 @@ __metadata:
"@uppy/provider-views": "workspace:^"
"@uppy/remote-sources": "workspace:^"
"@uppy/screen-capture": "workspace:^"
"@uppy/status-bar": "workspace:^"
"@uppy/store-default": "workspace:^"
"@uppy/thumbnail-generator": "workspace:^"
"@uppy/transloadit": "workspace:^"