Merge branch 'main' into playwright-upgrade

* main:
  Dedupe dependencies (#6085)
  [ci] release (#6087)
  build(deps): bump next from 15.5.2 to 15.5.7 (#6088)
  Introduce @uppy/image-generator (#6056)
This commit is contained in:
Murderlon 2025-12-05 10:22:49 +01:00
commit fa35f7b7ea
No known key found for this signature in database
GPG key ID: 1FF861FF1DDBB953
38 changed files with 967 additions and 214 deletions

View file

@ -2,7 +2,7 @@
Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can
use this from a CDN
(`<script src="https://releases.transloadit.com/uppy/v5.1.12/uppy.min.js"></script>`)
(`<script src="https://releases.transloadit.com/uppy/v5.2.0/uppy.min.js"></script>`)
or bundle it with your webapp.
Note that the recommended way to use Uppy is to install it with yarn/npm and use

View file

@ -103,7 +103,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus
```
Add CSS
[uppy.min.css](https://releases.transloadit.com/uppy/v5.1.12/uppy.min.css),
[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.0/uppy.min.css),
either to your HTML pages `<head>` or include in JS, if your bundler of choice
supports it.
@ -117,7 +117,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
```html
<!-- 1. Add CSS to `<head>` -->
<link
href="https://releases.transloadit.com/uppy/v5.1.12/uppy.min.css"
href="https://releases.transloadit.com/uppy/v5.2.0/uppy.min.css"
rel="stylesheet"
/>
@ -128,7 +128,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
Uppy,
Dashboard,
Tus,
} from 'https://releases.transloadit.com/uppy/v5.1.12/uppy.min.mjs'
} from 'https://releases.transloadit.com/uppy/v5.2.0/uppy.min.mjs'
const uppy = new Uppy()
uppy.use(Dashboard, { target: '#files-drag-drop' })

View file

@ -16,7 +16,7 @@
"@uppy/transloadit": "workspace:*",
"@uppy/tus": "workspace:*",
"@uppy/xhr-upload": "workspace:*",
"next": "15.5.2",
"next": "15.5.7",
"react": "19.1.0",
"react-dom": "19.1.0"
},

View file

@ -0,0 +1,14 @@
# @uppy/image-generator
## 1.0.0
### Major Changes
- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit
### Patch Changes
- Updated dependencies [5684efa]
- Updated dependencies [5684efa]
- @uppy/provider-views@5.2.1
- @uppy/transloadit@5.4.0

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 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,49 @@
# @uppy/image-generator
<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/image-generator.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/image-generator)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/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)
**[Read the docs](https://uppy.io/docs/image-generator)** |
**[Try it](https://uppy.io/examples/)**
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 ImageGenerator from '@uppy/image-generator'
const uppy = new Uppy()
.use(ImageGenerator, {
assemblyOptions: async (prompt) => {
const res = await fetch(`/assembly-options?prompt=${encodeURIComponent(prompt)}`)
return res.json()
}
})
```
## Installation
```bash
$ npm install @uppy/image-generator
```
Alternatively, you can also use this plugin in a pre-built bundle from
Transloadits CDN: Edgly. 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/image-generator).
## License
[The MIT License](./LICENSE).

View file

@ -0,0 +1,60 @@
{
"name": "@uppy/image-generator",
"description": "AI-powered image generation for Uppy",
"version": "1.0.0",
"license": "MIT",
"sideEffects": false,
"type": "module",
"keywords": [
"file uploader",
"upload",
"uppy",
"uppy-plugin",
"ai",
"image generation"
],
"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"
},
"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"
},
"dependencies": {
"@uppy/provider-views": "workspace:^",
"@uppy/transloadit": "workspace:^",
"@uppy/utils": "workspace:^",
"preact": "^10.5.13"
},
"devDependencies": {
"@uppy/core": "workspace:^",
"cssnano": "^7.0.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"sass": "^1.89.2"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,381 @@
import type { Body, Meta, MinimalRequiredUppyFile, Uppy } from '@uppy/core'
import { UIPlugin, type UIPluginOptions } from '@uppy/core'
import { FilterInput, SearchView } from '@uppy/provider-views'
import {
Assembly,
type AssemblyResult,
Client,
type OptionsWithRestructuredFields,
} from '@uppy/transloadit'
import { RateLimitedQueue } from '@uppy/utils'
import type { h } from 'preact'
import locale from './locale.js'
export interface ImageGeneratorOptions extends UIPluginOptions {
// OptionsWithRestructuredFields does not allow string[] for `fields`.
// in @uppy/transloadit we do accept that but then immediately use a type assertion to this type
// so that's why we just don't allow string[] from the start here
assemblyOptions: (prompt: string) => Promise<OptionsWithRestructuredFields>
}
interface PluginState extends Record<string, unknown> {
prompt: string
results: AssemblyResult[]
checkedResultIds: Set<AssemblyResult['id']>
loading: boolean
loadingMessageIndex: number
firstRun: boolean
}
const defaultState = {
prompt: '',
results: [],
checkedResultIds: new Set(),
loading: false,
loadingMessageIndex: 0,
firstRun: true,
} satisfies PluginState
const LOADING_MESSAGES = [
'generating1',
'generating2',
'generating3',
'generating4',
'generating5',
] as const
export default class ImageGenerator<
M extends Meta,
B extends Body,
> extends UIPlugin<ImageGeneratorOptions, M, B, PluginState> {
private loadingInterval: ReturnType<typeof setInterval> | null = null
private rateLimitedQueue: RateLimitedQueue
private client: Client<M, B>
private assembly: Assembly | null = null
icon: () => h.JSX.Element
constructor(uppy: Uppy<M, B>, opts: ImageGeneratorOptions) {
super(uppy, opts)
this.id = this.opts.id || 'ImageGenerator'
this.title = 'AI image'
this.type = 'acquirer'
this.defaultLocale = locale
this.rateLimitedQueue = new RateLimitedQueue(10)
this.client = new Client({
service: 'https://api2.transloadit.com',
rateLimitedQueue: this.rateLimitedQueue,
errorReporting: true,
})
this.setPluginState(defaultState)
this.i18nInit()
this.icon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="24"
height="24"
viewBox="0 0 24 24"
>
<title>ai-image-generator</title>
<defs>
<circle id="uppyImageGeneratorCircle" cx="12" cy="12" r="12" />
</defs>
<g fill="none" fillRule="evenodd">
<mask id="uppyImageGeneratorMask" fill="#fff">
<use xlinkHref="#uppyImageGeneratorCircle" />
</mask>
<use xlinkHref="#uppyImageGeneratorCircle" fill="#004b9d" />
<path
fill="#fff"
d="m21.98 15.453 2.793.254a.295.295 0 0 1 0 .586l-2.794.254a6 6 0 0 0-5.432 5.432l-.254 2.794a.295.295 0 0 1-.586 0l-.254-2.794a6 6 0 0 0-5.432-5.432l-2.794-.254a.295.295 0 0 1 0-.586l2.794-.254a6 6 0 0 0 5.432-5.432l.254-2.794a.295.295 0 0 1 .586 0l.254 2.794a6 6 0 0 0 5.432 5.432"
mask="url(#uppyImageGeneratorMask)"
/>
<path
fill="#fff"
d="m10.74 7.75 1.434.13a.121.121 0 0 1 0 .24l-1.433.13a2.75 2.75 0 0 0-2.49 2.49l-.13 1.434a.121.121 0 0 1-.242 0l-.13-1.433a2.75 2.75 0 0 0-2.49-2.49l-1.433-.13a.121.121 0 0 1 0-.242l1.433-.13a2.75 2.75 0 0 0 2.49-2.49l.13-1.433a.121.121 0 0 1 .242 0l.13 1.433a2.75 2.75 0 0 0 2.49 2.49"
mask="url(#uppyImageGeneratorMask)"
/>
</g>
</svg>
)
}
install(): void {
const { target } = this.opts
if (target) {
this.mount(target, this)
}
}
uninstall(): void {
this.clearLoadingInterval()
this.closeAssembly(true) // Cancel any in-progress assembly
this.unmount()
}
private closeAssembly(cancel = false): void {
if (this.assembly) {
const { status } = this.assembly
this.assembly.close()
this.assembly = null
// Cancel the assembly on the server to stop processing
if (cancel && status) {
this.client.cancelAssembly(status).catch(() => {
// If we can't cancel, there's not much we can do
})
}
}
}
private clearLoadingInterval(): void {
if (this.loadingInterval) {
clearInterval(this.loadingInterval)
this.loadingInterval = null
}
}
private startLoadingAnimation(): void {
this.clearLoadingInterval()
this.loadingInterval = setInterval(() => {
const { loadingMessageIndex } = this.getPluginState()
const nextIndex = (loadingMessageIndex + 1) % LOADING_MESSAGES.length
this.setPluginState({ loadingMessageIndex: nextIndex })
}, 4000)
}
/**
* Creates a Transloadit assembly to generate AI images.
*
* Completion scenarios:
* - Success: assembly emits 'finished' resolve() finally cleans up, keeps results
* - Error: assembly emits 'error' reject() catch reports error, finally cleans up
* - Dashboard close: onDashboardClose sets cancelled=true, resolve() finally resets state
* - Uninstall: closeAssembly(true) called directly, cancels server-side assembly
*/
generate = async () => {
const { loading, prompt } = this.getPluginState()
if (loading || prompt.trim() === '') return
const { promise, resolve, reject } = Promise.withResolvers<void>()
let cancelled = false
const onDashboardClose = () => {
cancelled = true
resolve()
}
// @ts-expect-error not typed because we do not depend on @uppy/dashboard
this.uppy.once('dashboard:close-panel', onDashboardClose)
try {
this.setPluginState({
loading: true,
results: [],
checkedResultIds: new Set(),
loadingMessageIndex: 0,
})
this.startLoadingAnimation()
const assemblyOptions = await this.opts.assemblyOptions(
this.getPluginState().prompt,
)
const assemblyResponse = await this.client.createAssembly({
params: assemblyOptions.params,
fields: assemblyOptions.fields ?? {},
signature: assemblyOptions.signature,
expectedFiles: 0,
})
const assembly = new Assembly(assemblyResponse, this.rateLimitedQueue)
this.assembly = assembly
assembly.on('result', (stepName: string, result: AssemblyResult) => {
const { results } = this.getPluginState()
this.setPluginState({
results: [...results, result],
firstRun: false,
})
})
assembly.on('error', reject)
assembly.on('finished', resolve)
assembly.connect()
await promise
} catch (error) {
this.client.submitError(error as Error).catch(() => {})
this.uppy.info('Image could not be generated', 'error')
throw error
} finally {
// @ts-expect-error not typed because we do not depend on @uppy/dashboard
this.uppy.off('dashboard:close-panel', onDashboardClose)
this.clearLoadingInterval()
this.closeAssembly(true)
this.setPluginState(cancelled ? defaultState : { loading: false })
}
}
private onCheckboxChange = (result: AssemblyResult, event?: Event) => {
if (event) {
event.stopPropagation()
event.preventDefault()
// Prevent shift-clicking from highlighting file names
document.getSelection()?.removeAllRanges()
}
const { checkedResultIds } = this.getPluginState()
if (checkedResultIds.has(result.id)) {
checkedResultIds.delete(result.id)
} else {
checkedResultIds.add(result.id)
}
this.setPluginState({ checkedResultIds })
}
private cancelSelection = () => {
this.setPluginState({ checkedResultIds: new Set() })
}
private donePicking = async () => {
const { checkedResultIds, results } = this.getPluginState()
const proms: Promise<MinimalRequiredUppyFile<M, B>>[] = results
.filter((result) => checkedResultIds.has(result.id))
.map(async (result) => {
const res = await fetch(result.ssl_url!)
const blob = await res.blob()
return {
name: `ai-image-${result.id!}`,
type: result.mime ?? undefined,
source: 'Transloadit',
data: blob,
}
})
const files = await Promise.all(proms)
this.uppy.addFiles(files)
this.setPluginState(defaultState)
}
render() {
const {
prompt,
results,
checkedResultIds,
loading,
loadingMessageIndex,
firstRun,
} = this.getPluginState()
const { i18n } = this.uppy
const currentLoadingMessage = loading
? i18n(LOADING_MESSAGES[loadingMessageIndex])
: undefined
if (firstRun) {
return (
<SearchView
value={prompt}
onChange={(prompt) => this.setPluginState({ prompt })}
onSubmit={this.generate}
inputLabel={i18n('generateImagePlaceholder')}
loading={loading}
>
{loading ? (
<span className="uppy-ImageGenerator-generating">
{currentLoadingMessage}
</span>
) : (
i18n('generateImage')
)}
</SearchView>
)
}
return (
<div className="uppy-ProviderBrowser uppy-ProviderBrowser-viewType--grid">
<FilterInput
value={prompt}
onChange={(prompt) => this.setPluginState({ prompt })}
onSubmit={this.generate}
inputLabel={i18n('search')}
i18n={i18n}
/>
{loading ? (
<div className="uppy-Provider-loading uppy-ImageGenerator-generating--darker">
{currentLoadingMessage}
</div>
) : results.length > 0 ? (
<div className="uppy-ProviderBrowser-body">
<ul className="uppy-ProviderBrowser-list" tabIndex={-1}>
{results.map((result) => (
<li
key={result.id}
className={`uppy-ProviderBrowserItem ${checkedResultIds.has(result.id) ? 'uppy-ProviderBrowserItem--is-checked' : ''}`}
>
<input
type="checkbox"
className="uppy-u-reset uppy-ProviderBrowserItem-checkbox uppy-ProviderBrowserItem-checkbox--grid"
onChange={(e) => this.onCheckboxChange(result, e)}
name="listitem"
id={result.id}
checked={checkedResultIds.has(result.id)}
data-uppy-super-focusable
/>
<label
htmlFor={result.id}
aria-label={prompt}
className="uppy-u-reset uppy-ProviderBrowserItem-inner"
>
<img
src={result.url!}
alt={prompt}
referrerPolicy="no-referrer"
loading="lazy"
/>
</label>
</li>
))}
</ul>
</div>
) : (
<div className="uppy-Provider-empty">{i18n('noSearchResults')}</div>
)}
{checkedResultIds.size > 0 && (
<div className="uppy-ProviderBrowser-footer">
<div className="uppy-ProviderBrowser-footer-buttons">
<button
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary"
onClick={this.donePicking}
type="button"
>
{i18n('selectX', {
smart_count: checkedResultIds.size,
})}
</button>
<button
className="uppy-u-reset uppy-c-btn uppy-c-btn-link"
onClick={this.cancelSelection}
type="button"
>
{i18n('cancel')}
</button>
</div>
</div>
)}
</div>
)
}
}

View file

@ -0,0 +1,12 @@
export default {
strings: {
generateImage: 'Generate image',
generateImagePlaceholder:
'A serene sunset over a mountain lake, with pine trees reflecting in the water',
generating1: 'AI is thinking...',
generating2: 'Crunching pixels...',
generating3: 'Summoning images...',
generating4: 'AI is working...',
generating5: 'Creating magic...',
},
}

View file

@ -0,0 +1,29 @@
@use '@uppy/core/src/_variables.scss' as *;
.uppy-ImageGenerator-generating {
background: linear-gradient(90deg, $gray-50, $gray-300, $gray-50);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: uppy-gradient-slide 3s ease-in-out infinite;
}
.uppy-ImageGenerator-generating--darker {
background: linear-gradient(90deg, $gray-500, $gray-800, $gray-500);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: uppy-gradient-slide 3s ease-in-out infinite;
}
@keyframes uppy-gradient-slide {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}

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,22 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true
},
"include": ["./package.json", "./src/**/*.*"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
},
{
"path": "../provider-views/tsconfig.build.json"
},
{
"path": "../transloadit/tsconfig.build.json"
}
]
}

View file

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

View file

@ -1,5 +1,11 @@
# @uppy/locales
## 5.1.0
### Minor Changes
- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit
## 5.0.1
### Patch Changes

View file

@ -1,7 +1,7 @@
{
"name": "@uppy/locales",
"description": "Uppy language packs",
"version": "5.0.1",
"version": "5.1.0",
"license": "MIT",
"type": "module",
"sideEffects": false,

View file

@ -44,6 +44,7 @@ en_US.strings = {
browseFolders: 'browse folders',
cancel: 'Cancel',
cancelUpload: 'Cancel upload',
chooseFiles: 'Choose files',
closeModal: 'Close Modal',
companionError: 'Connection with Companion failed',
companionUnauthorizeHint:
@ -100,6 +101,14 @@ en_US.strings = {
'1': 'Added %{smart_count} files from %{folder}',
},
folderAlreadyAdded: 'The folder "%{folder}" was already added',
generateImage: 'Generate image',
generateImagePlaceholder:
'A serene sunset over a mountain lake, with pine trees reflecting in the water',
generating1: 'AI is thinking...',
generating2: 'Crunching pixels...',
generating3: 'Summoning images...',
generating4: 'AI is working...',
generating5: 'Creating magic...',
generatingThumbnails: 'Generating thumbnails...',
import: 'Import',
importFiles: 'Import files from:',

View file

@ -4,6 +4,7 @@
"build": {
"dependsOn": [
"@uppy/image-editor#build",
"@uppy/image-generator#build",
"@uppy/box#build",
"@uppy/core#build",
"@uppy/google-drive-picker#build",

View file

@ -1,5 +1,11 @@
# @uppy/provider-views
## 5.2.1
### Patch Changes
- 5684efa: Refactor internal components
## 5.2.0
### Minor Changes

View file

@ -1,7 +1,7 @@
{
"name": "@uppy/provider-views",
"description": "View library for Uppy remote provider plugins.",
"version": "5.2.0",
"version": "5.2.1",
"license": "MIT",
"style": "dist/style.min.css",
"type": "module",

View file

@ -0,0 +1,68 @@
import type { I18n } from '@uppy/utils'
import { useSearchForm } from './useSearchForm.js'
interface FilterInputProps {
value: string
onChange: (value: string) => void
onSubmit: () => void
inputLabel: string
i18n: I18n
}
/**
* FilterInput component for client-side filtering with search icon and clear button.
* Supports Enter key submission via form element.
*/
function FilterInput({
value,
onChange,
onSubmit,
inputLabel,
i18n,
}: FilterInputProps) {
const { formId } = useSearchForm(onSubmit)
return (
<section className="uppy-ProviderBrowser-searchFilter">
<input
className="uppy-u-reset uppy-ProviderBrowser-searchFilterInput"
type="search"
aria-label={inputLabel}
placeholder={inputLabel}
value={value}
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
form={formId}
data-uppy-super-focusable
/>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon uppy-ProviderBrowser-searchFilterIcon"
width="12"
height="12"
viewBox="0 0 12 12"
>
<path d="M8.638 7.99l3.172 3.172a.492.492 0 1 1-.697.697L7.91 8.656a4.977 4.977 0 0 1-2.983.983C2.206 9.639 0 7.481 0 4.819 0 2.158 2.206 0 4.927 0c2.721 0 4.927 2.158 4.927 4.82a4.74 4.74 0 0 1-1.216 3.17zm-3.71.685c2.176 0 3.94-1.726 3.94-3.856 0-2.129-1.764-3.855-3.94-3.855C2.75.964.984 2.69.984 4.819c0 2.13 1.765 3.856 3.942 3.856z" />
</svg>
{value && (
<button
className="uppy-u-reset uppy-ProviderBrowser-searchFilterReset"
type="button"
aria-label={i18n('resetFilter')}
onClick={() => onChange('')}
>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
viewBox="0 0 19 19"
>
<path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
</svg>
</button>
)}
</section>
)
}
export default FilterInput

View file

@ -17,8 +17,8 @@ import debounce from 'lodash/debounce.js'
import type { h } from 'preact'
import packageJson from '../../package.json' with { type: 'json' }
import Browser from '../Browser.js'
import FilterInput from '../FilterInput.js'
import FooterActions from '../FooterActions.js'
import SearchInput from '../SearchInput.js'
import addFiles from '../utils/addFiles.js'
import getClickedRange from '../utils/getClickedRange.js'
import handleError from '../utils/handleError.js'
@ -678,14 +678,12 @@ export default class ProviderView<M extends Meta, B extends Body> {
i18n={i18n}
/>
{opts.showFilter && (
<SearchInput
searchString={searchString}
setSearchString={(s: string) => this.onSearchInput(s)}
submitSearchString={() => {}}
<FilterInput
value={searchString}
onChange={(s: string) => this.onSearchInput(s)}
onSubmit={() => {}}
inputLabel={i18n('filter')}
clearSearchLabel={i18n('resetFilter')}
wrapperClassName="uppy-ProviderBrowser-searchFilter"
inputClassName="uppy-ProviderBrowser-searchFilterInput"
i18n={i18n}
/>
)}

View file

@ -1,122 +0,0 @@
import { nanoid } from 'nanoid/non-secure'
import { useCallback, useEffect, useState } from 'preact/hooks'
type Props = {
searchString: string
setSearchString: (s: string) => void
submitSearchString: () => void
wrapperClassName: string
inputClassName: string
inputLabel: string
clearSearchLabel?: string
showButton?: boolean
buttonLabel?: string
buttonCSSClassName?: string
}
function SearchInput({
searchString,
setSearchString,
submitSearchString,
wrapperClassName,
inputClassName,
inputLabel,
clearSearchLabel = '',
showButton = false,
buttonLabel = '',
buttonCSSClassName = '',
}: Props) {
const onInput = (e: Event) => {
setSearchString((e.target as HTMLInputElement).value)
}
const submit = useCallback(
(ev: Event) => {
ev.preventDefault()
submitSearchString()
},
[submitSearchString],
)
// We do this to avoid nested <form>s
// (See https://github.com/transloadit/uppy/pull/5050#discussion_r1640392516)
const [form] = useState(() => {
const formEl = document.createElement('form')
formEl.setAttribute('tabindex', '-1')
formEl.id = nanoid()
return formEl
})
useEffect(() => {
document.body.appendChild(form)
form.addEventListener('submit', submit)
return () => {
form.removeEventListener('submit', submit)
document.body.removeChild(form)
}
}, [form, submit])
return (
<section className={wrapperClassName}>
<input
className={`uppy-u-reset ${inputClassName}`}
type="search"
aria-label={inputLabel}
placeholder={inputLabel}
value={searchString}
onInput={onInput}
form={form.id}
data-uppy-super-focusable
/>
{!showButton && (
// 🔍
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon uppy-ProviderBrowser-searchFilterIcon"
width="12"
height="12"
viewBox="0 0 12 12"
>
<path d="M8.638 7.99l3.172 3.172a.492.492 0 1 1-.697.697L7.91 8.656a4.977 4.977 0 0 1-2.983.983C2.206 9.639 0 7.481 0 4.819 0 2.158 2.206 0 4.927 0c2.721 0 4.927 2.158 4.927 4.82a4.74 4.74 0 0 1-1.216 3.17zm-3.71.685c2.176 0 3.94-1.726 3.94-3.856 0-2.129-1.764-3.855-3.94-3.855C2.75.964.984 2.69.984 4.819c0 2.13 1.765 3.856 3.942 3.856z" />
</svg>
)}
{!showButton && searchString && (
// ❌
<button
className="uppy-u-reset uppy-ProviderBrowser-searchFilterReset"
type="button"
aria-label={clearSearchLabel}
title={clearSearchLabel}
onClick={() => setSearchString('')}
>
<svg
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
viewBox="0 0 19 19"
>
<path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
</svg>
</button>
)}
{showButton && (
<button
className={`uppy-u-reset uppy-c-btn uppy-c-btn-primary ${buttonCSSClassName}`}
type="submit"
form={form.id}
>
{buttonLabel}
</button>
)}
</section>
)
}
export default SearchInput

View file

@ -16,8 +16,9 @@ import classNames from 'classnames'
import type { h } from 'preact'
import packageJson from '../../package.json' with { type: 'json' }
import Browser from '../Browser.js'
import FilterInput from '../FilterInput.js'
import FooterActions from '../FooterActions.js'
import SearchInput from '../SearchInput.js'
import SearchView from '../SearchView.js'
import addFiles from '../utils/addFiles.js'
import getClickedRange from '../utils/getClickedRange.js'
import handleError from '../utils/handleError.js'
@ -278,17 +279,14 @@ export default class SearchProviderView<M extends Meta, B extends Body> {
if (isInputMode) {
return (
<SearchInput
searchString={searchString}
setSearchString={this.setSearchString}
submitSearchString={this.search}
<SearchView
value={searchString}
onChange={this.setSearchString}
onSubmit={this.search}
inputLabel={i18n('enterTextToSearch')}
buttonLabel={i18n('searchImages')}
wrapperClassName="uppy-SearchProvider"
inputClassName="uppy-c-textInput uppy-SearchProvider-input"
showButton
buttonCSSClassName="uppy-SearchProvider-searchButton"
/>
>
{i18n('searchImages')}
</SearchView>
)
}
@ -300,14 +298,12 @@ export default class SearchProviderView<M extends Meta, B extends Body> {
)}
>
{opts.showFilter && (
<SearchInput
searchString={searchString}
setSearchString={this.setSearchString}
submitSearchString={this.search}
<FilterInput
value={searchString}
onChange={this.setSearchString}
onSubmit={this.search}
inputLabel={i18n('search')}
clearSearchLabel={i18n('resetSearch')}
wrapperClassName="uppy-ProviderBrowser-searchFilter"
inputClassName="uppy-ProviderBrowser-searchFilterInput"
i18n={i18n}
/>
)}

View file

@ -0,0 +1,53 @@
import type { ComponentChild } from 'preact'
import { useSearchForm } from './useSearchForm.js'
interface SearchViewProps {
value: string
onChange: (value: string) => void
onSubmit: () => void
inputLabel: string
loading?: boolean
children: ComponentChild
}
/**
* SearchView component for search with a submit button.
* Typically used for initial search views or forms that require explicit submission.
* The children prop is rendered as the button content, allowing full control over button text and loading states.
*/
function SearchView({
value,
onChange,
onSubmit,
inputLabel,
loading = false,
children,
}: SearchViewProps) {
const { formId } = useSearchForm(onSubmit)
return (
<section className="uppy-SearchProvider">
<input
className="uppy-u-reset uppy-c-textInput uppy-SearchProvider-input"
type="search"
aria-label={inputLabel}
placeholder={inputLabel}
value={value}
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
form={formId}
disabled={loading}
data-uppy-super-focusable
/>
<button
disabled={loading}
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-SearchProvider-searchButton"
type="submit"
form={formId}
>
{children}
</button>
</section>
)
}
export default SearchView

View file

@ -1,3 +1,4 @@
export { default as FilterInput } from './FilterInput.js'
export { default as GooglePickerView } from './GooglePicker/GooglePickerView.js'
export type {
MediaItem,
@ -24,5 +25,6 @@ export {
default as ProviderViews,
defaultPickerIcon,
} from './ProviderView/index.js'
export { default as SearchInput } from './SearchInput.js'
export { default as SearchProviderViews } from './SearchProviderView/index.js'
export { default as SearchView } from './SearchView.js'
export { useSearchForm } from './useSearchForm.js'

View file

@ -0,0 +1,42 @@
import { nanoid } from 'nanoid/non-secure'
import { useCallback, useEffect, useState } from 'preact/hooks'
/**
* Hook to create a form element outside the component tree to avoid nested forms.
* Returns a formId that can be used with the `form` attribute on inputs and buttons.
*
* This allows form submission (Enter key) to work properly even when the component
* is rendered inside another form element.
*
* @param onSubmit - Callback to execute when the form is submitted
* @returns Object containing the formId to use with form attribute
*/
export function useSearchForm(onSubmit: () => void): { formId: string } {
const submit = useCallback(
(ev: Event) => {
ev.preventDefault()
onSubmit()
},
[onSubmit],
)
// We create a form element and append it to document.body to avoid nested <form>s
// (See https://github.com/transloadit/uppy/pull/5050#discussion_r1640392516)
const [form] = useState(() => {
const formEl = document.createElement('form')
formEl.setAttribute('tabindex', '-1')
formEl.id = nanoid()
return formEl
})
useEffect(() => {
document.body.appendChild(form)
form.addEventListener('submit', submit)
return () => {
form.removeEventListener('submit', submit)
document.body.removeChild(form)
}
}, [form, submit])
return { formId: form.id }
}

View file

@ -1,5 +1,11 @@
# @uppy/transloadit
## 5.4.0
### Minor Changes
- 5684efa: Export Assembly, AssemblyError, Client
## 5.3.0
### Minor Changes

View file

@ -1,7 +1,7 @@
{
"name": "@uppy/transloadit",
"description": "The Transloadit plugin can be used to upload files to Transloadit for all kinds of processing, such as transcoding video, resizing images, zipping/unzipping, and more",
"version": "5.3.0",
"version": "5.4.0",
"license": "MIT",
"type": "module",
"sideEffects": false,

View file

@ -819,7 +819,9 @@ export default class Transloadit<
: this.opts.assemblyOptions
) as OptionsWithRestructuredFields
assemblyOptions.fields ??= {}
assemblyOptions.fields = {
...(assemblyOptions.fields ?? {}),
}
validateParams(assemblyOptions.params)
try {
@ -1032,3 +1034,7 @@ export default class Transloadit<
}
export { COMPANION_URL, COMPANION_ALLOWED_HOSTS }
// Low-level classes for advanced usage (e.g., creating assemblies without file uploads)
export { default as Assembly } from './Assembly.js'
export { AssemblyError, default as Client } from './Client.js'

View file

@ -1,5 +1,13 @@
# @uppy/webdav
## 1.1.1
### Patch Changes
- 5684efa: Refactor internal components
- Updated dependencies [5684efa]
- @uppy/provider-views@5.2.1
## 1.1.0
### Minor Changes

View file

@ -1,7 +1,7 @@
{
"name": "@uppy/webdav",
"description": "Import files from WebDAV into Uppy.",
"version": "1.1.0",
"version": "1.1.1",
"license": "MIT",
"types": "types/index.d.ts",
"type": "module",

View file

@ -14,7 +14,7 @@ import type {
} from '@uppy/core'
import { UIPlugin } from '@uppy/core'
import { ProviderViews, SearchInput } from '@uppy/provider-views'
import { ProviderViews, SearchView } from '@uppy/provider-views'
import type { I18n, LocaleStrings } from '@uppy/utils'
// biome-ignore lint/style/useImportType: h is not a type
import { type ComponentChild, h } from 'preact'
@ -61,17 +61,14 @@ const AuthForm = ({
}, [onAuth, webdavUrl])
return (
<SearchInput
searchString={webdavUrl}
setSearchString={setWebdavUrl}
submitSearchString={onSubmit}
<SearchView
value={webdavUrl}
onChange={setWebdavUrl}
onSubmit={onSubmit}
inputLabel={i18n('pluginWebdavInputLabel')}
buttonLabel={i18n('authenticate')}
wrapperClassName="uppy-SearchProvider"
inputClassName="uppy-c-textInput uppy-SearchProvider-input"
showButton
buttonCSSClassName="uppy-SearchProvider-searchButton"
/>
>
{i18n('authenticate')}
</SearchView>
)
}

View file

@ -1,5 +1,22 @@
# uppy
## 5.2.0
### Minor Changes
- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit
### Patch Changes
- Updated dependencies [5684efa]
- Updated dependencies [5684efa]
- Updated dependencies [5684efa]
- @uppy/provider-views@5.2.1
- @uppy/webdav@1.1.1
- @uppy/transloadit@5.4.0
- @uppy/image-generator@1.0.0
- @uppy/locales@5.1.0
## 5.1.12
### Patch Changes

View file

@ -1,7 +1,7 @@
{
"name": "uppy",
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
"version": "5.1.12",
"version": "5.2.0",
"license": "MIT",
"main": "lib/index.js",
"module": "lib/index.js",
@ -54,6 +54,7 @@
"@uppy/google-drive-picker": "workspace:*",
"@uppy/google-photos-picker": "workspace:*",
"@uppy/image-editor": "workspace:*",
"@uppy/image-generator": "workspace:*",
"@uppy/instagram": "workspace:*",
"@uppy/locales": "workspace:*",
"@uppy/onedrive": "workspace:*",

View file

@ -8,4 +8,5 @@
@use '@uppy/screen-capture/src/style.scss' as screen-capture;
@use '@uppy/status-bar/src/style.scss' as status-bar;
@use '@uppy/image-editor/src/style.scss' as image-editor;
@use '@uppy/image-generator/src/style.scss' as image-generator;
@use '@uppy/drop-target/src/style.scss' as drop-target;

View file

@ -10,6 +10,7 @@ import GoogleDrive from '@uppy/google-drive'
import GoogleDrivePicker from '@uppy/google-drive-picker'
import GooglePhotosPicker from '@uppy/google-photos-picker'
import ImageEditor from '@uppy/image-editor'
import ImageGenerator from '@uppy/image-generator'
import english from '@uppy/locales/lib/en_US.js'
import RemoteSources from '@uppy/remote-sources'
import ScreenCapture from '@uppy/screen-capture'
@ -174,6 +175,25 @@ export default () => {
.use(ScreenCapture, { target: Dashboard })
.use(Form, { target: '#upload-form' })
.use(ImageEditor, { target: Dashboard })
.use(ImageGenerator, {
target: Dashboard,
assemblyOptions: async (prompt) =>
// never create a signature on the client in production!
// it will expose the secret on the client
generateSignatureIfSecret(TRANSLOADIT_SECRET, {
auth: { key: TRANSLOADIT_KEY },
steps: {
generated_image: {
robot: '/image/generate',
result: true,
aspect_ratio: '2:3',
model: 'flux-1.1-pro-ultra',
prompt,
num_outputs: 2,
},
},
}),
})
.use(DropTarget, {
target: document.body,
})

View file

@ -23,6 +23,12 @@
],
"outputs": ["lib/**", "dist/**"]
},
"uppy#build:css": {
"outputLogs": "new-only",
"dependsOn": ["^build:css"],
"inputs": ["src/**/*.scss"],
"outputs": ["lib/**/*.css", "dist/**/*.css"]
},
"build:css": {
"outputLogs": "new-only",
"inputs": ["src/**/*.scss"],

105
yarn.lock
View file

@ -4958,65 +4958,65 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:15.5.2":
version: 15.5.2
resolution: "@next/env@npm:15.5.2"
checksum: 10/1e1c4f5b725165663460bae67de95cab624c66a865395a0af98405d3302483bebed8e79fcc2dc1c447b7010b5519fddb49670de16b00b75d679b52b29a4d86f5
"@next/env@npm:15.5.7":
version: 15.5.7
resolution: "@next/env@npm:15.5.7"
checksum: 10/11f971691018bd62a5bf253fc843fb2a6cf1431468f5c3a9d4d41753a6ff3e8bf7f539f46aba3f58f8bac59e681bf05fb5d771ac08d7dbd966a601257e1368bf
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-darwin-arm64@npm:15.5.2"
"@next/swc-darwin-arm64@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-darwin-arm64@npm:15.5.7"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-darwin-x64@npm:15.5.2"
"@next/swc-darwin-x64@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-darwin-x64@npm:15.5.7"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-linux-arm64-gnu@npm:15.5.2"
"@next/swc-linux-arm64-gnu@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-linux-arm64-gnu@npm:15.5.7"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-linux-arm64-musl@npm:15.5.2"
"@next/swc-linux-arm64-musl@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-linux-arm64-musl@npm:15.5.7"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-linux-x64-gnu@npm:15.5.2"
"@next/swc-linux-x64-gnu@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-linux-x64-gnu@npm:15.5.7"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-linux-x64-musl@npm:15.5.2"
"@next/swc-linux-x64-musl@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-linux-x64-musl@npm:15.5.7"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-win32-arm64-msvc@npm:15.5.2"
"@next/swc-win32-arm64-msvc@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-win32-arm64-msvc@npm:15.5.7"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:15.5.2":
version: 15.5.2
resolution: "@next/swc-win32-x64-msvc@npm:15.5.2"
"@next/swc-win32-x64-msvc@npm:15.5.7":
version: 15.5.7
resolution: "@next/swc-win32-x64-msvc@npm:15.5.7"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -9139,6 +9139,24 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/image-generator@workspace:*, @uppy/image-generator@workspace:packages/@uppy/image-generator":
version: 0.0.0-use.local
resolution: "@uppy/image-generator@workspace:packages/@uppy/image-generator"
dependencies:
"@uppy/core": "workspace:^"
"@uppy/provider-views": "workspace:^"
"@uppy/transloadit": "workspace:^"
"@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"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/instagram@workspace:*, @uppy/instagram@workspace:^, @uppy/instagram@workspace:packages/@uppy/instagram":
version: 0.0.0-use.local
resolution: "@uppy/instagram@workspace:packages/@uppy/instagram"
@ -9362,7 +9380,7 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/transloadit@workspace:*, @uppy/transloadit@workspace:packages/@uppy/transloadit":
"@uppy/transloadit@workspace:*, @uppy/transloadit@workspace:^, @uppy/transloadit@workspace:packages/@uppy/transloadit":
version: 0.0.0-use.local
resolution: "@uppy/transloadit@workspace:packages/@uppy/transloadit"
dependencies:
@ -13268,7 +13286,7 @@ __metadata:
"@uppy/transloadit": "workspace:*"
"@uppy/tus": "workspace:*"
"@uppy/xhr-upload": "workspace:*"
next: "npm:15.5.2"
next: "npm:15.5.7"
react: "npm:19.1.0"
react-dom: "npm:19.1.0"
tailwindcss: "npm:^4"
@ -17660,19 +17678,19 @@ __metadata:
languageName: node
linkType: hard
"next@npm:15.5.2":
version: 15.5.2
resolution: "next@npm:15.5.2"
"next@npm:15.5.7":
version: 15.5.7
resolution: "next@npm:15.5.7"
dependencies:
"@next/env": "npm:15.5.2"
"@next/swc-darwin-arm64": "npm:15.5.2"
"@next/swc-darwin-x64": "npm:15.5.2"
"@next/swc-linux-arm64-gnu": "npm:15.5.2"
"@next/swc-linux-arm64-musl": "npm:15.5.2"
"@next/swc-linux-x64-gnu": "npm:15.5.2"
"@next/swc-linux-x64-musl": "npm:15.5.2"
"@next/swc-win32-arm64-msvc": "npm:15.5.2"
"@next/swc-win32-x64-msvc": "npm:15.5.2"
"@next/env": "npm:15.5.7"
"@next/swc-darwin-arm64": "npm:15.5.7"
"@next/swc-darwin-x64": "npm:15.5.7"
"@next/swc-linux-arm64-gnu": "npm:15.5.7"
"@next/swc-linux-arm64-musl": "npm:15.5.7"
"@next/swc-linux-x64-gnu": "npm:15.5.7"
"@next/swc-linux-x64-musl": "npm:15.5.7"
"@next/swc-win32-arm64-msvc": "npm:15.5.7"
"@next/swc-win32-x64-msvc": "npm:15.5.7"
"@swc/helpers": "npm:0.5.15"
caniuse-lite: "npm:^1.0.30001579"
postcss: "npm:8.4.31"
@ -17715,7 +17733,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: 10/0e5a7420e7ea9bba57b32575378c9d74eb4cc95a34453d0e88dd6212f95d7d4f9fd35a106dba0ca5f537e3265a3ab48cf93487f7c792502ee83da3a9bb4be92c
checksum: 10/bfac0cbac41b36227ec91d3a0727561f73dfc35d6a78eafd0200528ef95fa2e9d2dba5a8c8922864b4f4e4321ae6a07c6cf8f5766ac3192ad03e68516c3a458a
languageName: node
linkType: hard
@ -22649,6 +22667,7 @@ __metadata:
"@uppy/google-drive-picker": "workspace:*"
"@uppy/google-photos-picker": "workspace:*"
"@uppy/image-editor": "workspace:*"
"@uppy/image-generator": "workspace:*"
"@uppy/instagram": "workspace:*"
"@uppy/locales": "workspace:*"
"@uppy/onedrive": "workspace:*"