mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
Headless components (#5727)
Co-authored-by: Mikael Finstad <finstaden@gmail.com>
This commit is contained in:
parent
d1a3345263
commit
0259b09d73
108 changed files with 4006 additions and 1004 deletions
114
.cursor/rules/headless-components.mdc
Normal file
114
.cursor/rules/headless-components.mdc
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
description:
|
||||
globs: packages/@uppy/components/src/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# Headless components
|
||||
|
||||
You are an expert at making headless UI components in Preact, similar to libraries like shadcn, except we don't rely on packages like radix.
|
||||
|
||||
## Goal
|
||||
|
||||
Making headless components in Preact for the open source Uppy file uploader and framework specific hooks. We want to give flexbility to users of this library by rendering sensible UI defaults and hooks that abstract Uppy functionality to completely build your own UI.
|
||||
|
||||
Another way to give flexibility is to add data attributes selectively to some HTML elements. These can be used with CSS selectors by users of this library to conditionally apply styles:
|
||||
|
||||
```html
|
||||
<button
|
||||
type="button"
|
||||
data-uppy-element="upload-button"
|
||||
data-state={ctx.status}
|
||||
></button>
|
||||
```
|
||||
|
||||
## How to build these components
|
||||
|
||||
It's important to understand that an automated build script (bin/build-components.mjs) generates framework-specific wrappers for these Preact components.
|
||||
|
||||
Here is an example from the React wrapper created by the script.
|
||||
|
||||
```tsx
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
type DropzoneProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function Dropzone(props: Omit<DropzoneProps, 'ctx' | 'render'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactDropzone, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies DropzoneProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
```
|
||||
|
||||
You don't have to worry about how these wrappers are created but it's important to know when building the Preact components that you will always receive a `ctx` prop to work with. This is its type:
|
||||
|
||||
```ts
|
||||
import type Uppy from '@uppy/core'
|
||||
export type UploadStatus =
|
||||
| 'init'
|
||||
| 'ready'
|
||||
| 'uploading'
|
||||
| 'paused'
|
||||
| 'error'
|
||||
| 'complete'
|
||||
export type UppyContext = {
|
||||
uppy: Uppy | undefined
|
||||
status: UploadStatus
|
||||
progress: number
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Styling is done with Tailwind CSS 4.x. There is no Tailwind config file.
|
||||
|
||||
**IMPORTANT**: all classes have the `uppy:` prefix. Example: `bg-red-500` should become `uppy:bg-red-500`.
|
||||
|
||||
Use `clsx` for conditional styles.
|
||||
|
||||
```js
|
||||
import clsx from 'clsx';
|
||||
// or
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
// Strings (variadic)
|
||||
clsx('foo', true && 'bar', 'baz');
|
||||
//=> 'foo bar baz'
|
||||
|
||||
// Objects
|
||||
clsx({ foo:true, bar:false, baz:isTrue() });
|
||||
//=> 'foo baz'
|
||||
|
||||
// Objects (variadic)
|
||||
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
|
||||
//=> 'foo --foobar'
|
||||
|
||||
// Arrays
|
||||
clsx(['foo', 0, false, 'bar']);
|
||||
//=> 'foo bar'
|
||||
|
||||
// Arrays (variadic)
|
||||
clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
|
||||
//=> 'foo bar baz hello there'
|
||||
|
||||
// Kitchen sink (with nesting)
|
||||
clsx('foo', [1 && 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');
|
||||
//=> 'foo bar hello world cya'
|
||||
```
|
||||
78
.cursor/rules/svelte.mdc
Normal file
78
.cursor/rules/svelte.mdc
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
description:
|
||||
globs: packages/@uppy/svelte/**,examples/sveltekit/**
|
||||
alwaysApply: false
|
||||
---
|
||||
I'm using svelte 5 instead of svelte 4 here is an overview of the changes.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
Svelte 5 introduces runes, a set of advanced primitives for controlling reactivity. The runes replace certain non-runes features and provide more explicit control over state and effects.
|
||||
|
||||
Snippets, along with render tags, help create reusable chunks of markup inside your components, reducing duplication and enhancing maintainability.
|
||||
|
||||
## Event Handlers in Svelte 5
|
||||
|
||||
In Svelte 5, event handlers are treated as standard HTML properties rather than Svelte-specific directives, simplifying their use and integrating them more closely with the rest of the properties in the component.
|
||||
|
||||
### Svelte 4 vs. Svelte 5:
|
||||
|
||||
**Before (Svelte 4):**
|
||||
```html
|
||||
<script>
|
||||
let count = 0;
|
||||
$: double = count * 2;
|
||||
$: {
|
||||
if (count > 10) alert('Too high!');
|
||||
}
|
||||
</script>
|
||||
<button on:click={() => count++}> {count} / {double}</button>
|
||||
```
|
||||
|
||||
**After (Svelte 5):**
|
||||
```html
|
||||
<script>
|
||||
import { $state, $effect, $derived } from 'svelte';
|
||||
|
||||
// Define state with runes
|
||||
let count = $state(0);
|
||||
|
||||
// Option 1: Using $derived for computed values
|
||||
let double = $derived(count * 2);
|
||||
|
||||
// Reactive effects using runes
|
||||
$effect(() => {
|
||||
if (count > 10) alert('Too high!');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Standard HTML event attributes instead of Svelte directives -->
|
||||
<button onclick={() => count++}>
|
||||
{count} / {double}
|
||||
</button>
|
||||
|
||||
<!-- Alternatively, you can compute values inline -->
|
||||
<!-- <button onclick={() => count++}>
|
||||
{count} / {count * 2}
|
||||
</button> -->
|
||||
```
|
||||
|
||||
## Key Differences:
|
||||
|
||||
1. **Reactivity is Explicit**:
|
||||
- Svelte 5 uses `$state()` to explicitly mark reactive variables
|
||||
- `$derived()` replaces `$:` for computed values
|
||||
- `$effect()` replaces `$: {}` blocks for side effects
|
||||
|
||||
2. **Event Handling is Standardized**:
|
||||
- Svelte 4: `on:click={handler}`
|
||||
- Svelte 5: `onclick={handler}`
|
||||
|
||||
3. **Import Runes**:
|
||||
- All runes must be imported from 'svelte': `import { $state, $effect, $derived, $props, $slots } from 'svelte';`
|
||||
|
||||
4. **No More Event Modifiers**:
|
||||
- Svelte 4: `on:click|preventDefault={handler}`
|
||||
- Svelte 5: `onclick={e => { e.preventDefault(); handler(e); }}`
|
||||
|
||||
This creates clearer, more maintainable components compared to Svelte 4's previous syntax by making reactivity explicit and using standardized web platform features.
|
||||
149
.cursor/rules/uppy-core.mdc
Normal file
149
.cursor/rules/uppy-core.mdc
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
description: Read this when you are about to use the Uppy class from @uppy/core
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Uppy Core Summary
|
||||
|
||||
If you need to reference or work with the `Uppy` class from `@uppy/core`, here is an high-level overview of the class.
|
||||
|
||||
## `Uppy<M extends Meta, B extends Body>` Class
|
||||
|
||||
A modular file uploader. Manages plugins, state, events, and file handling.
|
||||
|
||||
**Generics:**
|
||||
|
||||
* `M`: Metadata object shape associated with files (`Meta`).
|
||||
* `B`: Body object shape associated with files (`Body`).
|
||||
|
||||
**Key Public Properties:**
|
||||
|
||||
* `opts: NonNullableUppyOptions<M, B>`: The fully resolved Uppy options.
|
||||
* `store: Store<State<M, B>>`: The state management store (defaults to `DefaultStore`).
|
||||
* `i18n: I18n`: The translation function.
|
||||
* `i18nArray: Translator['translateArray']`: The array translation function.
|
||||
* `locale: Locale`: The active locale object.
|
||||
|
||||
**Constructor:**
|
||||
|
||||
* `constructor(opts?: UppyOptionsWithOptionalRestrictions<M, B>)`
|
||||
|
||||
**Core Public Methods:**
|
||||
|
||||
* `use<T extends typeof BasePlugin<any, M, B>>(Plugin: T, ...args: OmitFirstArg<ConstructorParameters<T>>): this`: Adds a plugin instance.
|
||||
* `getPlugin<T extends UnknownPlugin<M, B> = UnknownPlugin<M, B>>(id: string): T | undefined`: Retrieves a plugin instance by ID.
|
||||
* `iteratePlugins(method: (plugin: UnknownPlugin<M, B>) => void): void`: Executes a function on all installed plugins.
|
||||
* `removePlugin(instance: UnknownPlugin<M, B>): void`: Removes a plugin instance.
|
||||
* `addFile(file: File | MinimalRequiredUppyFile<M, B>): UppyFile<M, B>['id']`: Adds a single file.
|
||||
* `addFiles(fileDescriptors: MinimalRequiredUppyFile<M, B>[]): void`: Adds multiple files.
|
||||
* `removeFile(fileID: string): void`: Removes a file by ID.
|
||||
* `removeFiles(fileIDs: string[]): void`: Removes multiple files by ID.
|
||||
* `getFile(fileID: string): UppyFile<M, B>`: Retrieves a file object by ID.
|
||||
* `getFiles(): UppyFile<M, B>[]`: Retrieves all file objects as an array.
|
||||
* `setOptions(newOpts: MinimalRequiredOptions<M, B>): void`: Updates Uppy options.
|
||||
* `setState(patch?: Partial<State<M, B>>): void`: Updates the Uppy state.
|
||||
* `getState(): State<M, B>`: Retrieves the current Uppy state.
|
||||
* `setFileState(fileID: string, state: Partial<UppyFile<M, B>>): void`: Updates the state for a specific file.
|
||||
* `setMeta(data: Partial<M>): void`: Merges metadata into the global `state.meta` and all file `meta` objects.
|
||||
* `setFileMeta(fileID: string, data: Partial<M>): void`: Merges metadata into a specific file's `meta` object.
|
||||
* `upload(): Promise<NonNullable<UploadResult<M, B>> | undefined>`: Starts the upload process for all new files.
|
||||
* `retryUpload(fileID: string): Promise<UploadResult<M, B> | undefined>`: Retries a failed upload for a specific file.
|
||||
* `retryAll(): Promise<UploadResult<M, B> | undefined>`: Retries all failed uploads.
|
||||
* `cancelAll(): void`: Cancels all uploads and removes all files.
|
||||
* `pauseResume(fileID: string): boolean | undefined`: Toggles pause/resume state for a resumable upload.
|
||||
* `pauseAll(): void`: Pauses all resumable uploads.
|
||||
* `resumeAll(): void`: Resumes all paused uploads.
|
||||
* `info(message: string | { message: string; details?: string | Record<string, string> }, type?: LogLevel, duration?: number): void`: Displays an informational message via UI plugins.
|
||||
* `hideInfo(): void`: Hides the oldest info message.
|
||||
* `log(message: unknown, type?: 'error' | 'warning'): void`: Logs a message using the configured logger.
|
||||
* `on<K extends keyof UppyEventMap<M, B>>(event: K, callback: UppyEventMap<M, B>[K]): this`: Registers an event listener.
|
||||
* `off<K extends keyof UppyEventMap<M, B>>(event: K, callback: UppyEventMap<M, B>[K]): this`: Unregisters an event listener.
|
||||
* `emit<T extends keyof UppyEventMap<M, B>>(event: T, ...args: Parameters<UppyEventMap<M, B>[T]>): void`: Emits an event.
|
||||
* `destroy(): void`: Uninstalls all plugins and cleans up the Uppy instance.
|
||||
|
||||
**Internal Lifecycle Methods (Called by Uppy):**
|
||||
|
||||
* `addPreProcessor(fn: Processor): void`
|
||||
* `removePreProcessor(fn: Processor): boolean`
|
||||
* `addPostProcessor(fn: Processor): void`
|
||||
* `removePostProcessor(fn: Processor): boolean`
|
||||
* `addUploader(fn: Processor): void`
|
||||
* `removeUploader(fn: Processor): boolean`
|
||||
|
||||
---
|
||||
|
||||
## Key Associated Types
|
||||
|
||||
* **`UppyFile<M extends Meta, B extends Body>`**: Represents a file within Uppy.
|
||||
* `id: string`: Unique file ID.
|
||||
* `source?: string`: Source plugin ID.
|
||||
* `name: string`: File name.
|
||||
* `type?: string`: MIME type.
|
||||
* `data: Blob | File`: The actual file data.
|
||||
* `meta: M & { name: string, type: string }`: User and Uppy metadata.
|
||||
* `size: number | null`: File size in bytes.
|
||||
* `isRemote: boolean`: If the file is from a remote source (Companion).
|
||||
* `remote?: { requestClientId: string, ... }`: Remote source details.
|
||||
* `progress: FileProgressStarted | FileProgressNotStarted`: Upload progress state.
|
||||
* `error?: string`: Error message if upload failed.
|
||||
* `isPaused?: boolean`: If upload is paused (for resumable uploads).
|
||||
* `isGhost?: boolean`: If file is restored without data (Golden Retriever).
|
||||
* `response?: { status: number, body: B, uploadURL?: string, ... }`: Upload response details.
|
||||
* `preview?: string`: URL to a preview image.
|
||||
* `[key: string]: any`: Extensible with plugin-specific state.
|
||||
|
||||
* **`State<M extends Meta, B extends Body>`**: The main Uppy state object.
|
||||
* `files: { [fileID: string]: UppyFile<M, B> }`: Object map of files by ID.
|
||||
* `currentUploads: { [uploadID: string]: CurrentUpload<M, B> }`: Map of active uploads.
|
||||
* `capabilities: { uploadProgress: boolean, individualCancellation: boolean, resumableUploads: boolean, ... }`: Detected/configured capabilities.
|
||||
* `totalProgress: number`: Overall upload progress (0-100).
|
||||
* `meta: M`: Global metadata.
|
||||
* `info: Array<{ type: LogLevel, message: string, details?: string | Record<string, string> }>`: Info messages for UI display.
|
||||
* `error: string | null`: Global error message.
|
||||
* `allowNewUpload: boolean`: Whether new uploads can be started.
|
||||
* `plugins: { [pluginID: string]: Record<string, unknown> }`: State managed by plugins.
|
||||
* `recoveredState: null | { files: ..., currentUploads: ... }`: State recovered by Golden Retriever.
|
||||
|
||||
* **`UppyOptions<M extends Meta, B extends Body>`**: Options for the Uppy constructor.
|
||||
* `id?: string`: Instance ID (default: 'uppy').
|
||||
* `autoProceed?: boolean`: Start upload automatically after files are added (default: false).
|
||||
* `allowMultipleUploadBatches?: boolean`: Allow adding files while an upload is in progress (default: true).
|
||||
* `debug?: boolean`: Enable debug logging (default: false).
|
||||
* `restrictions: Restrictions`: File restrictions (type, size, number).
|
||||
* `meta?: M`: Initial global metadata.
|
||||
* `onBeforeFileAdded?: (currentFile: UppyFile<M, B>, files: { ... }) => UppyFile<M, B> | boolean | undefined`: Hook before a file is added.
|
||||
* `onBeforeUpload?: (files: { ... }) => { ... } | boolean`: Hook before an upload starts.
|
||||
* `locale?: Locale`: Custom locale strings.
|
||||
* `store?: Store<State<M, B>>`: Custom state store.
|
||||
* `logger?: { debug, warn, error }`: Custom logger.
|
||||
* `infoTimeout?: number`: Duration for info messages (ms).
|
||||
|
||||
* **`Restrictions`**: File restriction options.
|
||||
* `maxFileSize?: number | null`: Max individual file size (bytes).
|
||||
* `minFileSize?: number | null`: Min individual file size (bytes).
|
||||
* `maxTotalFileSize?: number | null`: Max total size of all files (bytes).
|
||||
* `maxNumberOfFiles?: number | null`: Max number of files allowed.
|
||||
* `minNumberOfFiles?: number | null`: Min number of files required.
|
||||
* `allowedFileTypes?: string[] | null`: Allowed MIME types or extensions (e.g., `['image/*', '.pdf']`).
|
||||
* `requiredMetaFields?: string[]`: Meta fields that must be present before upload.
|
||||
|
||||
* **`UppyEventMap<M extends Meta, B extends Body>`**: Map of event names to their callback signatures (includes events like `file-added`, `upload-progress`, `upload-success`, `complete`, `error`, `restriction-failed`, etc.).
|
||||
|
||||
* **`UploadResult<M extends Meta, B extends Body>`**: The object returned when an upload completes.
|
||||
* `successful?: UppyFile<M, B>[]`: Array of successfully uploaded files.
|
||||
* `failed?: UppyFile<M, B>[]`: Array of files that failed to upload.
|
||||
* `uploadID?: string`: The ID of the upload batch.
|
||||
|
||||
* **`BasePlugin<...> `**: The base class for all Uppy plugins.
|
||||
* `id: string`: Unique plugin ID.
|
||||
* `type: string`: Plugin type ('acquirer', 'modifier', 'uploader', 'presenter', 'orchestrator', 'logger').
|
||||
* `uppy: Uppy<M, B>`: Reference to the Uppy instance.
|
||||
* `install(): void`: Called when the plugin is added.
|
||||
* `uninstall(): void`: Called when the plugin is removed.
|
||||
* `update(state: Partial<State<M, B>>): void`: Called on every Uppy state update.
|
||||
|
||||
* **`Processor`**: Type for pre/post/upload processor functions `(fileIDs: string[], uploadID: string) => Promise<unknown> | void`.
|
||||
|
||||
* **`LogLevel`**: `'info' | 'warning' | 'error' | 'success'`.
|
||||
|
||||
* **`PartialTree`**: Type used by Provider plugins (like Google Drive) to represent folder structures (`(PartialTreeFile | PartialTreeFolder)[]`).
|
||||
|
|
@ -288,6 +288,7 @@ module.exports = {
|
|||
// TODO: update those to more modern code when switch to ESM is complete
|
||||
'examples/react-native-expo/*.js',
|
||||
'examples/svelte-example/**/*.js',
|
||||
'examples/sveltekit/**/*.js',
|
||||
'examples/vue/**/*.js',
|
||||
'examples/vue3/**/*.js',
|
||||
],
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@ yarn-error.log
|
|||
.env
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.build.tsbuildinfo
|
||||
.svelte-kit
|
||||
|
||||
dist/
|
||||
lib/
|
||||
|
|
|
|||
266
bin/build-components.mjs
Normal file
266
bin/build-components.mjs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Get the directory of this script
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(scriptDir, '..')
|
||||
|
||||
// Define paths
|
||||
const COMPONENTS_DIR = path.join(rootDir, 'packages/@uppy/components/src')
|
||||
const REACT_DIR = path.join(rootDir, 'packages/@uppy/react/src/headless')
|
||||
const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless')
|
||||
const SVELTE_DIR = path.join(
|
||||
rootDir,
|
||||
'packages/@uppy/svelte/src/lib/components/headless',
|
||||
)
|
||||
|
||||
// Templates
|
||||
const REACT_TEMPLATE = `import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function %%ComponentName%%(props: Omit<%%PropsTypeName%%, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(%%PreactComponentName%%, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies %%PropsTypeName%%),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
`
|
||||
|
||||
const VUE_TEMPLATE = `import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
||||
name: '%%ComponentName%%',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function render%%ComponentName%%() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(%%PreactComponentName%%, {
|
||||
...(attrs as %%PropsTypeName%%),
|
||||
ctx,
|
||||
} satisfies %%PropsTypeName%%),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
render%%ComponentName%%()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
render%%ComponentName%%()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
render%%ComponentName%%()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
`
|
||||
|
||||
const SVELTE_TEMPLATE = `<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<%%PropsTypeName%%, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(%%PreactComponentName%%, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies %%PropsTypeName%%),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
`
|
||||
|
||||
try {
|
||||
// Check if components directory exists
|
||||
await fs.access(COMPONENTS_DIR).catch(() => {
|
||||
throw new Error(`Components directory not found: ${COMPONENTS_DIR}`)
|
||||
})
|
||||
await Promise.all(
|
||||
[REACT_DIR, VUE_DIR, SVELTE_DIR].map(async (dir) => {
|
||||
if (!existsSync(dir)) {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// Read all files in components directory
|
||||
const files = await fs.readdir(COMPONENTS_DIR)
|
||||
|
||||
// Filter for .tsx files
|
||||
const tsxFiles = files.filter((file) => file.endsWith('.tsx'))
|
||||
|
||||
console.log(`Found ${tsxFiles.length} Preact component(s) to process\n`)
|
||||
|
||||
// Track generated components for index files
|
||||
const reactComponents = []
|
||||
const vueComponents = []
|
||||
const svelteComponents = []
|
||||
|
||||
// Process each tsx file
|
||||
for (const file of tsxFiles) {
|
||||
try {
|
||||
const componentName = path.basename(file, '.tsx')
|
||||
const propsTypeName = `${componentName}Props`
|
||||
const preactComponentName = `Preact${componentName}`
|
||||
|
||||
// Generate React wrapper
|
||||
const reactContent = REACT_TEMPLATE.replace(
|
||||
/%%ComponentName%%/g,
|
||||
componentName,
|
||||
)
|
||||
.replace(/%%PreactComponentName%%/g, preactComponentName)
|
||||
.replace(/%%PropsTypeName%%/g, propsTypeName)
|
||||
|
||||
// Generate Vue wrapper
|
||||
const vueContent = VUE_TEMPLATE.replace(
|
||||
/%%ComponentName%%/g,
|
||||
componentName,
|
||||
)
|
||||
.replace(/%%PreactComponentName%%/g, preactComponentName)
|
||||
.replace(/%%PropsTypeName%%/g, propsTypeName)
|
||||
|
||||
// Generate Svelte wrapper
|
||||
const svelteContent = SVELTE_TEMPLATE.replace(
|
||||
/%%ComponentName%%/g,
|
||||
componentName,
|
||||
)
|
||||
.replace(/%%PreactComponentName%%/g, preactComponentName)
|
||||
.replace(/%%PropsTypeName%%/g, propsTypeName)
|
||||
|
||||
// Write files
|
||||
const reactFilePath = path.join(REACT_DIR, `${componentName}.tsx`)
|
||||
const vueFilePath = path.join(VUE_DIR, `${componentName}.ts`)
|
||||
const svelteFilePath = path.join(SVELTE_DIR, `${componentName}.svelte`)
|
||||
|
||||
await fs.writeFile(reactFilePath, reactContent)
|
||||
await fs.writeFile(vueFilePath, vueContent)
|
||||
await fs.writeFile(svelteFilePath, svelteContent)
|
||||
|
||||
// Add to component lists for index files
|
||||
reactComponents.push(componentName)
|
||||
vueComponents.push(componentName)
|
||||
svelteComponents.push(componentName)
|
||||
|
||||
console.log(`✔︎ ${componentName}`)
|
||||
} catch (error) {
|
||||
console.error(`Error processing component ${file}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate index files
|
||||
if (reactComponents.length > 0) {
|
||||
const reactIndexContent = reactComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.js'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(REACT_DIR, 'index.ts'), reactIndexContent)
|
||||
console.log(`\nExporting React components from ${REACT_DIR}`)
|
||||
}
|
||||
|
||||
if (vueComponents.length > 0) {
|
||||
const vueIndexContent = vueComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.js'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(VUE_DIR, 'index.ts'), vueIndexContent)
|
||||
console.log(`Exporting Vue components from ${VUE_DIR}`)
|
||||
}
|
||||
|
||||
if (svelteComponents.length > 0) {
|
||||
const svelteIndexContent = svelteComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.svelte'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(SVELTE_DIR, 'index.ts'), svelteIndexContent)
|
||||
console.log(`Exporting Svelte components from ${SVELTE_DIR}`)
|
||||
}
|
||||
|
||||
// Copy CSS file
|
||||
const CSS_SOURCE_PATH = path.join(
|
||||
rootDir,
|
||||
'packages/@uppy/components/dist/styles.css',
|
||||
)
|
||||
const FRAMEWORK_DIRS_FOR_CSS = [
|
||||
path.join(rootDir, 'packages/@uppy/react/dist'),
|
||||
path.join(rootDir, 'packages/@uppy/vue/dist'),
|
||||
path.join(rootDir, 'packages/@uppy/svelte/dist'),
|
||||
]
|
||||
|
||||
try {
|
||||
await fs.access(CSS_SOURCE_PATH) // Check if source CSS exists
|
||||
await Promise.all(
|
||||
FRAMEWORK_DIRS_FOR_CSS.map(async (destDir) => {
|
||||
if (!existsSync(destDir)) {
|
||||
await fs.mkdir(destDir, { recursive: true })
|
||||
}
|
||||
const destPath = path.join(destDir, 'styles.css')
|
||||
await fs.copyFile(CSS_SOURCE_PATH, destPath)
|
||||
}),
|
||||
)
|
||||
} catch (cssError) {
|
||||
console.error(
|
||||
`${CSS_SOURCE_PATH} does not exist yet. Run \`yarn build\` first.`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log('\nAll wrappers and index files generated successfully!')
|
||||
} catch (error) {
|
||||
console.error('Error generating wrappers:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
@ -1,22 +1,53 @@
|
|||
<template>
|
||||
<dashboard :uppy="uppy" />
|
||||
<UppyContextProvider :uppy="uppy">
|
||||
<main>
|
||||
<h1>Vue Headless Components Test</h1>
|
||||
|
||||
<article id="files-list">
|
||||
<h2>With list</h2>
|
||||
<Dropzone />
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article id="files-grid">
|
||||
<h2>With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid :columns="2" />
|
||||
<UploadButton />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Uppy from '@uppy/core'
|
||||
import { Dashboard } from '@uppy/vue'
|
||||
import Tus from '@uppy/tus'
|
||||
import {
|
||||
UppyContextProvider,
|
||||
Dropzone,
|
||||
FilesList,
|
||||
FilesGrid,
|
||||
UploadButton,
|
||||
} from '@uppy/vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Dashboard,
|
||||
UppyContextProvider,
|
||||
Dropzone,
|
||||
FilesList,
|
||||
FilesGrid,
|
||||
UploadButton,
|
||||
},
|
||||
computed: {
|
||||
uppy: () => new Uppy(),
|
||||
uppy: () =>
|
||||
new Uppy().use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="@uppy/core/dist/style.css"></style>
|
||||
<style src="@uppy/dashboard/dist/style.css"></style>
|
||||
<style src="@uppy/vue/dist/styles.css"></style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
describe('dashboard-vue', () => {
|
||||
describe('@uppy/vue', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/dashboard-vue')
|
||||
cy.get('input[type="file"]').first().as('file-input')
|
||||
})
|
||||
|
||||
// Only Vue 3 works in Parcel if you use SFC's but Vue 3 is broken in Uppy:
|
||||
// https://github.com/transloadit/uppy/issues/2877
|
||||
xit('should render in Vue 3 and show thumbnails', () => {
|
||||
it('should render headless components in Vue 3 correctly', () => {
|
||||
cy.get('@file-input').selectFile(
|
||||
[
|
||||
'cypress/fixtures/images/cat.jpg',
|
||||
|
|
@ -13,8 +12,38 @@ describe('dashboard-vue', () => {
|
|||
],
|
||||
{ force: true },
|
||||
)
|
||||
cy.get('.uppy-Dashboard-Item-previewImg')
|
||||
.should('have.length', 2)
|
||||
.each((element) => expect(element).attr('src').to.include('blob:'))
|
||||
|
||||
// Test FilesList shows files correctly
|
||||
cy.get('ul[data-uppy-element="files-list"]').should('exist')
|
||||
cy.get('ul[data-uppy-element="files-list"] li').should('have.length', 2)
|
||||
|
||||
// Test FilesGrid shows files correctly
|
||||
cy.get('div[data-uppy-element="files-grid"]').should('exist')
|
||||
cy.get('div[data-uppy-element="files-grid"] div.uppy-reset').should(
|
||||
'have.length',
|
||||
2,
|
||||
)
|
||||
|
||||
// Test UploadButton is functional
|
||||
cy.get('#files-grid button[data-uppy-element="upload-button"]')
|
||||
.should('exist')
|
||||
.and('contain', 'Upload')
|
||||
.and('not.be.disabled')
|
||||
.click()
|
||||
|
||||
// Check if button shows progress during upload
|
||||
cy.get('#files-grid button[data-uppy-element="upload-button"] span').should(
|
||||
'contain',
|
||||
'Uploaded',
|
||||
)
|
||||
// Check if cancel button appears during upload
|
||||
cy.get('#files-grid button[data-uppy-element="cancel-button"]')
|
||||
.should('exist')
|
||||
.and('contain', 'Cancel')
|
||||
|
||||
cy.get('#files-grid button[data-uppy-element="upload-button"] span').should(
|
||||
'contain',
|
||||
'Complete',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import Uppy, {
|
||||
UIPlugin,
|
||||
type Meta,
|
||||
type Body,
|
||||
type UIPluginOptions,
|
||||
type State,
|
||||
} from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
import Webcam from '@uppy/webcam'
|
||||
import { Dashboard, useUppyState } from '@uppy/react'
|
||||
|
||||
import '@uppy/core/dist/style.css'
|
||||
import '@uppy/dashboard/dist/style.css'
|
||||
import '@uppy/webcam/dist/style.css'
|
||||
|
||||
interface MyPluginOptions extends UIPluginOptions {}
|
||||
|
||||
interface MyPluginState extends Record<string, unknown> {}
|
||||
|
||||
// Custom plugin example inside React
|
||||
class MyPlugin<M extends Meta, B extends Body> extends UIPlugin<
|
||||
MyPluginOptions,
|
||||
M,
|
||||
B,
|
||||
MyPluginState
|
||||
> {
|
||||
container!: HTMLElement
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts?: MyPluginOptions) {
|
||||
super(uppy, opts)
|
||||
this.type = 'acquirer'
|
||||
this.id = this.opts.id || 'TEST'
|
||||
this.title = 'Test'
|
||||
}
|
||||
|
||||
override install() {
|
||||
const { target } = this.opts
|
||||
if (target) {
|
||||
this.mount(target, this)
|
||||
}
|
||||
}
|
||||
|
||||
override uninstall() {
|
||||
this.unmount()
|
||||
}
|
||||
|
||||
override render(state: State<M, B>, container: HTMLElement) {
|
||||
// Important: during the initial render is not defined. Safely return.
|
||||
if (!container) return
|
||||
createRoot(container).render(
|
||||
<h2>React component inside Uppy's Preact UI</h2>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const metaFields = [
|
||||
{ id: 'license', name: 'License', placeholder: 'specify license' },
|
||||
]
|
||||
|
||||
function createUppy() {
|
||||
return new Uppy({ restrictions: { requiredMetaFields: ['license'] } })
|
||||
.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' })
|
||||
.use(Webcam)
|
||||
.use(MyPlugin)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// IMPORTANT: passing an initaliser function to useState
|
||||
// to prevent creating a new Uppy instance on every render.
|
||||
// useMemo is a performance hint, not a guarantee.
|
||||
const [uppy] = React.useState(createUppy)
|
||||
// You can access state reactively with useUppyState
|
||||
const fileCount = useUppyState(
|
||||
uppy,
|
||||
(state) => Object.keys(state.files).length,
|
||||
)
|
||||
const totalProgress = useUppyState(uppy, (state) => state.totalProgress)
|
||||
// Also possible to get the state of all plugins.
|
||||
const plugins = useUppyState(uppy, (state) => state.plugins)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>File count: {fileCount}</p>
|
||||
<p>Total progress: {totalProgress}</p>
|
||||
<Dashboard uppy={uppy} metaFields={metaFields} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# React example
|
||||
|
||||
This is minimal example created to demonstrate how to integrate Uppy in your
|
||||
React app.
|
||||
|
||||
To spawn the demo, use the following commands:
|
||||
|
||||
```sh
|
||||
corepack yarn install
|
||||
corepack yarn build
|
||||
corepack yarn workspace @uppy-example/react dev
|
||||
```
|
||||
|
||||
If you'd like to use a different package manager than Yarn (e.g. npm) to work
|
||||
with this example, you can extract it from the workspace like this:
|
||||
|
||||
```sh
|
||||
corepack yarn workspace @uppy-example/react pack
|
||||
|
||||
# The above command should have create a .tgz file, we're going to extract it to
|
||||
# a new directory outside of the Uppy workspace.
|
||||
mkdir ../react-example
|
||||
tar -xzf examples/react-example/package.tgz -C ../react-example --strip-components 1
|
||||
rm -f examples/react-example/package.tgz
|
||||
|
||||
# Now you can leave the Uppy workspace and use the example as a standalone JS project:
|
||||
cd ../react-example
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Uppy React Example</title>
|
||||
<link href="uppy.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./main.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.querySelector('#app')!).render(<App />)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"name": "@uppy-example/react",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/react": "workspace:*",
|
||||
"@uppy/remote-sources": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^5.4.17"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsxImportSource": "react",
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2022",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"module": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"sourceMap": true,
|
||||
"lib": ["es2022", "dom", "dom.iterable"],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
12
examples/react/index.html
Normal file
12
examples/react/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
examples/react/package.json
Normal file
27
examples/react/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@uppy-example/react",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@uppy/core": "^4.4.2",
|
||||
"@uppy/react": "workspace:^",
|
||||
"@uppy/tus": "^4.2.2",
|
||||
"@uppy/webcam": "^4.1.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
47
examples/react/src/App.tsx
Normal file
47
examples/react/src/App.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dropzone,
|
||||
FilesGrid,
|
||||
FilesList,
|
||||
UploadButton,
|
||||
UppyContextProvider,
|
||||
} from '@uppy/react'
|
||||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
|
||||
import './app.css'
|
||||
import '@uppy/react/dist/styles.css'
|
||||
|
||||
function App() {
|
||||
const [uppy] = useState(() =>
|
||||
new Uppy().use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<UppyContextProvider uppy={uppy}>
|
||||
<main className="p-5 max-w-xl mx-auto">
|
||||
<h1 className="text-4xl font-bold">Welcome to React.</h1>
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid columns={2} />
|
||||
<UploadButton />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
6
examples/react/src/app.css
Normal file
6
examples/react/src/app.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
/* example how to use the data attributes to apply custom styles */
|
||||
/* button[data-uppy-element="upload-button"][data-state="uploading"] {
|
||||
background-color: red;
|
||||
} */
|
||||
10
examples/react/src/main.tsx
Normal file
10
examples/react/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.js'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
1
examples/react/src/vite-env.d.ts
vendored
Normal file
1
examples/react/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
21
examples/react/tsconfig.json
Normal file
21
examples/react/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
},
|
||||
"include": ["src", "src/**/*.tsx"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
}
|
||||
8
examples/react/tsconfig.node.json
Normal file
8
examples/react/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
examples/react/vite.config.ts
Normal file
10
examples/react/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
// @ts-expect-error untyped for some reason but fine
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||
12
examples/svelte-example/.gitignore
vendored
12
examples/svelte-example/.gitignore
vendored
|
|
@ -1,12 +0,0 @@
|
|||
/node_modules/
|
||||
/uploads/
|
||||
|
||||
.DS_Store
|
||||
/build/
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Uppy with Svelte
|
||||
|
||||
## Run it
|
||||
|
||||
To run this example, make sure you've correctly installed the **repository
|
||||
root**:
|
||||
|
||||
```sh
|
||||
corepack yarn install
|
||||
corepack yarn build
|
||||
```
|
||||
|
||||
Then, again in the **repository root**, start this example by doing:
|
||||
|
||||
```sh
|
||||
corepack yarn workspace @uppy-example/svelte-app dev
|
||||
```
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"name": "@uppy-example/svelte-app",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all --parallel dev:frontend dev:backend",
|
||||
"dev:frontend": "vite dev",
|
||||
"dev:backend": "node --watch ./server.js",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/formidable": "^3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "~5.4",
|
||||
"vite": "^5.4.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/drag-drop": "workspace:*",
|
||||
"@uppy/progress-bar": "workspace:*",
|
||||
"@uppy/svelte": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*",
|
||||
"@uppy/xhr-upload": "workspace:*",
|
||||
"formidable": "^3.5.1"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import http from 'node:http'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
|
||||
import formidable from 'formidable'
|
||||
|
||||
const UPLOAD_DIR = new URL('./uploads/', import.meta.url)
|
||||
|
||||
await mkdir(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
http
|
||||
.createServer((req, res) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
|
||||
'Access-Control-Max-Age': 2592000, // 30 days
|
||||
/** add other headers as per requirement */
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, headers)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
if (req.url === '/upload' && req.method.toLowerCase() === 'post') {
|
||||
// parse a file upload
|
||||
const form = formidable({
|
||||
keepExtensions: true,
|
||||
uploadDir: fileURLToPath(UPLOAD_DIR),
|
||||
})
|
||||
|
||||
form.parse(req, (err, fields, files) => {
|
||||
res.writeHead(200, headers)
|
||||
if (err) {
|
||||
console.log('some error', err)
|
||||
res.write(JSON.stringify(err))
|
||||
} else {
|
||||
for (const {
|
||||
filepath,
|
||||
originalFilename,
|
||||
mimetype,
|
||||
size,
|
||||
} of files.files) {
|
||||
console.log('saved file', {
|
||||
filepath,
|
||||
originalFilename,
|
||||
mimetype,
|
||||
size,
|
||||
})
|
||||
}
|
||||
res.write(JSON.stringify({ fields, files }))
|
||||
}
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
})
|
||||
.listen(9967, () => {
|
||||
console.log('server started')
|
||||
})
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import '@uppy/core/dist/style.min.css'
|
||||
import '@uppy/dashboard/dist/style.min.css'
|
||||
import '@uppy/drag-drop/dist/style.min.css'
|
||||
import '@uppy/progress-bar/dist/style.min.css'
|
||||
|
||||
export const ssr = false
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Dashboard, DashboardModal, DragDrop, ProgressBar } from '@uppy/svelte'
|
||||
import Uppy from '@uppy/core'
|
||||
import Webcam from '@uppy/webcam'
|
||||
import XHRUpload from '@uppy/xhr-upload'
|
||||
|
||||
const createUppy = () => {
|
||||
return new Uppy().use(Webcam).use(XHRUpload, {
|
||||
bundle: true,
|
||||
endpoint: 'http://localhost:9967/upload',
|
||||
allowedMetaFields: ['something'],
|
||||
fieldName: 'files',
|
||||
})
|
||||
}
|
||||
|
||||
let uppy1 = createUppy()
|
||||
let uppy2 = createUppy()
|
||||
|
||||
let open = false;
|
||||
let showInlineDashboard = true;
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Welcome to the <code>@uppy/svelte</code> demo!</h1>
|
||||
<h2>Inline Dashboard</h2>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={showInlineDashboard}
|
||||
/>
|
||||
Show Dashboard
|
||||
</label>
|
||||
{#if showInlineDashboard}
|
||||
<Dashboard
|
||||
uppy={uppy1}
|
||||
plugins={['Webcam']}
|
||||
/>
|
||||
{/if}
|
||||
<h2>Modal Dashboard</h2>
|
||||
<div>
|
||||
<button on:click={() => open = true}>Show Dashboard</button>
|
||||
<DashboardModal
|
||||
uppy={uppy2}
|
||||
open={open}
|
||||
props={{
|
||||
onRequestCloseModal: () => open = false,
|
||||
plugins: ['Webcam']
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2>Drag Drop Area</h2>
|
||||
<DragDrop
|
||||
uppy={uppy1}
|
||||
/>
|
||||
|
||||
<h2>Progress Bar</h2>
|
||||
<ProgressBar
|
||||
uppy={uppy1}
|
||||
props={{
|
||||
hideAfterFinish: false
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
<style global>
|
||||
input[type="checkbox"] {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import adapter from '@sveltejs/adapter-static'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
23
examples/sveltekit/.gitignore
vendored
Normal file
23
examples/sveltekit/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
1
examples/sveltekit/.npmrc
Normal file
1
examples/sveltekit/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
41
examples/sveltekit/README.md
Normal file
41
examples/sveltekit/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by
|
||||
[`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or
|
||||
`pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an
|
||||
> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
34
examples/sveltekit/package.json
Normal file
34
examples/sveltekit/package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "@uppy-example/sveltekit",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/drag-drop": "workspace:*",
|
||||
"@uppy/progress-bar": "workspace:*",
|
||||
"@uppy/svelte": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.2.5"
|
||||
}
|
||||
}
|
||||
1
examples/sveltekit/src/app.css
Normal file
1
examples/sveltekit/src/app.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import 'tailwindcss';
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<app style="display: contents">%sveltekit.body%</app>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
7
examples/sveltekit/src/routes/+layout.svelte
Normal file
7
examples/sveltekit/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
36
examples/sveltekit/src/routes/+page.svelte
Normal file
36
examples/sveltekit/src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
import {
|
||||
UppyContextProvider,
|
||||
Dropzone,
|
||||
FilesList,
|
||||
FilesGrid,
|
||||
UploadButton,
|
||||
} from '@uppy/svelte'
|
||||
import '@uppy/svelte/dist/styles.css'
|
||||
|
||||
const uppy = new Uppy().use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
</script>
|
||||
|
||||
<UppyContextProvider {uppy}>
|
||||
<main class="p-5 max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold">Welcome to SvelteKit.</h1>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid columns={2} />
|
||||
<UploadButton />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
BIN
examples/sveltekit/static/favicon.png
Normal file
BIN
examples/sveltekit/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
18
examples/sveltekit/svelte.config.js
Normal file
18
examples/sveltekit/svelte.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -9,11 +9,11 @@
|
|||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
},
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
// eslint-disable-next-line import/no-unresolved
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
})
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/src/app.css" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "@uppy-example/vue3",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
|
@ -18,7 +19,9 @@
|
|||
"vue": "^3.2.33"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.4.17"
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,115 +1,42 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<!-- <HelloWorld msg="Welcome to Uppy Vue Demo"/> -->
|
||||
<h1>Welcome to Uppy Vue Demo!</h1>
|
||||
<h2>Inline Dashboard</h2>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="showInlineDashboard"
|
||||
@change="
|
||||
(event) => {
|
||||
showInlineDashboard = event.target.checked
|
||||
}
|
||||
"
|
||||
/>
|
||||
Show Dashboard
|
||||
</label>
|
||||
<Dashboard
|
||||
v-if="showInlineDashboard"
|
||||
:uppy="uppy"
|
||||
:props="{
|
||||
metaFields: [{ id: 'name', name: 'Name', placeholder: 'File name' }],
|
||||
}"
|
||||
/>
|
||||
<h2>Modal Dashboard</h2>
|
||||
<div>
|
||||
<button @click="open = true">Show Dashboard</button>
|
||||
<DashboardModal
|
||||
:uppy="uppy2"
|
||||
:open="open"
|
||||
:props="{
|
||||
onRequestCloseModal: handleClose,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<UppyContextProvider :uppy="uppy">
|
||||
<main class="p-5 max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold">Welcome to Vue.</h1>
|
||||
|
||||
<h2>Drag Drop Area</h2>
|
||||
<DragDrop
|
||||
:uppy="uppy"
|
||||
:props="{
|
||||
locale: {
|
||||
strings: {
|
||||
chooseFile: 'Boop a file',
|
||||
orDragDrop: 'or yoink it here',
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<h2>Progress Bar</h2>
|
||||
<ProgressBar
|
||||
:uppy="uppy"
|
||||
:props="{
|
||||
hideAfterFinish: false,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid :columns="2" />
|
||||
<UploadButton />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Dashboard, DashboardModal, DragDrop, ProgressBar } from '@uppy/vue'
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
import Webcam from '@uppy/webcam'
|
||||
import { defineComponent } from 'vue'
|
||||
import {
|
||||
UppyContextProvider,
|
||||
Dropzone,
|
||||
FilesList,
|
||||
FilesGrid,
|
||||
UploadButton,
|
||||
} from '@uppy/vue'
|
||||
|
||||
const { VITE_TUS_ENDPOINT: TUS_ENDPOINT } = import.meta.env
|
||||
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
uppy: () =>
|
||||
new Uppy({ id: 'uppy1', autoProceed: true, debug: true })
|
||||
.use(Tus, {
|
||||
endpoint: TUS_ENDPOINT,
|
||||
})
|
||||
.use(Webcam),
|
||||
uppy2: () =>
|
||||
new Uppy({ id: 'uppy2', autoProceed: false, debug: true })
|
||||
.use(Tus, {
|
||||
endpoint: TUS_ENDPOINT,
|
||||
})
|
||||
.use(Webcam),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
open: false,
|
||||
showInlineDashboard: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.open = false
|
||||
},
|
||||
},
|
||||
})
|
||||
const uppy = computed(() =>
|
||||
new Uppy().use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
}),
|
||||
)
|
||||
</script>
|
||||
|
||||
<style src="@uppy/core/dist/style.css"></style>
|
||||
<style src="@uppy/dashboard/dist/style.css"></style>
|
||||
<style src="@uppy/drag-drop/dist/style.css"></style>
|
||||
<style src="@uppy/progress-bar/dist/style.css"></style>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
<style src="@uppy/vue/dist/styles.css"></style>
|
||||
|
|
|
|||
1
examples/vue3/src/app.css
Normal file
1
examples/vue3/src/app.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import 'tailwindcss';
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url)
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
envDir: fileURLToPath(ROOT),
|
||||
plugins: [vue()],
|
||||
plugins: [vue(), tailwindcss()],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -111,10 +111,11 @@
|
|||
"scripts": {
|
||||
"start:companion": "bash bin/companion.sh",
|
||||
"start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
|
||||
"build:components": "node bin/build-components.mjs",
|
||||
"build:bundle": "yarn node ./bin/build-bundle.mjs",
|
||||
"build:clean": "cp .gitignore .gitignore.bak && printf '!node_modules\n!**/node_modules/**/*\n' >> .gitignore; git clean -Xfd packages e2e .parcel-cache coverage; mv .gitignore.bak .gitignore",
|
||||
"build:companion": "yarn workspace @uppy/companion build",
|
||||
"build:css": "yarn node ./bin/build-css.js",
|
||||
"build:css": "yarn node ./bin/build-css.js && yarn workspace @uppy/components build:tailwind",
|
||||
"build:svelte": "yarn workspace @uppy/svelte build",
|
||||
"build:angular": "yarn workspace angular build",
|
||||
"build:js:typeless": "npm-run-all build:lib build:companion build:svelte",
|
||||
|
|
@ -122,7 +123,7 @@
|
|||
"build:ts": "yarn tsc -b tsconfig.build.json && yarn workspace @uppy/svelte build",
|
||||
"build:lib": "yarn node ./bin/build-lib.mjs",
|
||||
"build:locale-pack": "yarn workspace @uppy-dev/locale-pack build && eslint packages/@uppy/locales/src/en_US.ts --fix && yarn workspace @uppy-dev/locale-pack test unused",
|
||||
"build": "npm-run-all --serial build:ts --parallel build:js build:css --serial size",
|
||||
"build": "npm-run-all --serial build:ts --parallel build:js build:css --serial build:components size",
|
||||
"contributors:save": "yarn node ./bin/update-contributors.mjs",
|
||||
"dev:with-companion": "npm-run-all --parallel start:companion dev",
|
||||
"dev": "yarn workspace @uppy-dev/dev dev",
|
||||
|
|
@ -156,7 +157,7 @@
|
|||
"test": "npm-run-all lint test:locale-packs:unused test:unit test:companion",
|
||||
"uploadcdn": "yarn node ./private/upload-to-cdn/index.js",
|
||||
"version": "yarn node ./bin/after-version-bump.js",
|
||||
"watch:css": "onchange 'packages/{@uppy/,}*/src/*.scss' --initial --verbose -- yarn run build:css",
|
||||
"watch:css": "onchange 'packages/{@uppy/,}*/src/*.{scss,css}' --initial --verbose -- yarn run build:css",
|
||||
"watch:js:bundle": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx}' --initial --verbose -- yarn run build:bundle",
|
||||
"watch:js:lib": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx}' --initial --verbose -- yarn run build:lib",
|
||||
"watch:js": "npm-run-all --parallel watch:js:bundle watch:js:lib",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"sourceMap": false,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmitOnError": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
|
|
|
|||
57
packages/@uppy/components/package.json
Normal file
57
packages/@uppy/components/package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "@uppy/components",
|
||||
"description": "Headless Uppy components, made in Preact",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"uppy",
|
||||
"uppy-plugin",
|
||||
"headless",
|
||||
"components",
|
||||
"preact"
|
||||
],
|
||||
"homepage": "https://uppy.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/transloadit/uppy/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build:tailwind": "npx @tailwindcss/cli -i ./src/input.css -o ./dist/styles.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.0.6",
|
||||
"clsx": "^2.1.1",
|
||||
"preact": "^10.5.13",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"tailwindcss": "^4.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/audio": "workspace:^",
|
||||
"@uppy/core": "workspace:^",
|
||||
"@uppy/image-editor": "workspace:^",
|
||||
"@uppy/screen-capture": "workspace:^",
|
||||
"@uppy/webcam": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@uppy/audio": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/google-drive-picker": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/image-editor": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/screen-capture": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/webcam": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
119
packages/@uppy/components/src/Dropzone.tsx
Normal file
119
packages/@uppy/components/src/Dropzone.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { h } from 'preact'
|
||||
import { useState, useRef } from 'preact/hooks'
|
||||
import { clsx } from 'clsx'
|
||||
import type { UppyContext } from './types.js'
|
||||
|
||||
export type DropzoneProps = {
|
||||
width?: string
|
||||
height?: string
|
||||
note?: string
|
||||
noClick?: boolean
|
||||
ctx: UppyContext
|
||||
}
|
||||
|
||||
export default function Dropzone(props: DropzoneProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isDragging, setIsDragging] = useState(() => false)
|
||||
const { width, height, note, noClick, ctx } = props
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(false)
|
||||
const files = Array.from(event.dataTransfer?.files || [])
|
||||
if (!files.length) return
|
||||
ctx.uppy?.addFiles(
|
||||
files.map((file) => ({
|
||||
source: 'drag-drop',
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (noClick) return
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
function handleFileInputChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
ctx.uppy?.addFiles(
|
||||
files.map((file) => ({
|
||||
source: 'drag-drop',
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
|
||||
// Reset the input value so the same file can be selected again
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="uppy-reset" data-uppy-element="dropzone">
|
||||
<input
|
||||
type="file"
|
||||
className="uppy:hidden"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
width: width || '100%',
|
||||
height: height || '100%',
|
||||
}}
|
||||
className={clsx(
|
||||
'uppy:border-2 uppy:border-dashed uppy:border-gray-300',
|
||||
'uppy:rounded-lg uppy:p-6 uppy:bg-gray-50',
|
||||
'uppy:transition-colors uppy:duration-200',
|
||||
{
|
||||
'uppy:bg-blue-50': isDragging,
|
||||
},
|
||||
{
|
||||
'uppy:cursor-pointer uppy:hover:bg-blue-50': !noClick,
|
||||
},
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="uppy:flex uppy:flex-col uppy:items-center uppy:justify-center uppy:h-full uppy:space-y-3">
|
||||
<p className="uppy:text-gray-600">
|
||||
Drop files here or click to add them
|
||||
</p>
|
||||
</div>
|
||||
{note ?
|
||||
<div className="uppy:text-sm uppy:text-gray-500">{note}</div>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
packages/@uppy/components/src/FilesGrid.tsx
Normal file
102
packages/@uppy/components/src/FilesGrid.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
import { Fragment, h } from 'preact'
|
||||
import { useState, useEffect } from 'preact/hooks'
|
||||
|
||||
import type { Body, Meta, UppyEventMap, UppyFile } from '@uppy/core'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { clsx } from 'clsx'
|
||||
import { Thumbnail } from './index.js'
|
||||
import type { UppyContext } from './types.js'
|
||||
|
||||
export type FilesGridProps = {
|
||||
editFile?: (file: UppyFile<Meta, Body>) => void
|
||||
columns?: number
|
||||
ctx: UppyContext
|
||||
}
|
||||
|
||||
export default function FilesGrid(props: FilesGridProps) {
|
||||
const [files, setFiles] = useState<UppyFile<Meta, Body>[]>(() => [])
|
||||
const { ctx, editFile } = props
|
||||
|
||||
function gridColsClass() {
|
||||
return (
|
||||
{
|
||||
1: 'uppy:grid-cols-1',
|
||||
2: 'uppy:grid-cols-2',
|
||||
3: 'uppy:grid-cols-3',
|
||||
4: 'uppy:grid-cols-4',
|
||||
5: 'uppy:grid-cols-5',
|
||||
6: 'uppy:grid-cols-6',
|
||||
}[props.columns || 2] || 'uppy:grid-cols-2'
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onStateUpdate: UppyEventMap<any, any>['state-update'] = (
|
||||
prev,
|
||||
next,
|
||||
patch,
|
||||
) => {
|
||||
if (patch?.files) {
|
||||
setFiles(Object.values(patch.files))
|
||||
}
|
||||
}
|
||||
ctx.uppy?.on('state-update', onStateUpdate)
|
||||
return () => {
|
||||
ctx.uppy?.off('state-update', onStateUpdate)
|
||||
}
|
||||
}, [ctx.uppy])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-uppy-element="files-grid"
|
||||
className={clsx(
|
||||
'uppy:reset uppy:my-4 uppy:grid uppy:gap-4',
|
||||
gridColsClass(),
|
||||
)}
|
||||
>
|
||||
{files?.map((file) => (
|
||||
<div
|
||||
className="uppy:flex uppy:flex-col uppy:items-center uppy:gap-2"
|
||||
key={file.id}
|
||||
>
|
||||
<Fragment>
|
||||
<Thumbnail images file={file} />
|
||||
<div className="uppy:w-full uppy-reset">
|
||||
<p className="uppy:font-medium uppy:truncate" title={file.name}>
|
||||
{file.name}
|
||||
</p>
|
||||
<div className="uppy:flex uppy:items-center uppy:gap-2">
|
||||
<p className=" uppy:text-gray-500 uppy:tabular-nums ">
|
||||
{prettyBytes(file.size || 0)}
|
||||
</p>
|
||||
|
||||
{editFile && (
|
||||
<button
|
||||
type="button"
|
||||
className="uppy:flex uppy:rounded uppy:text-blue-500 uppy:hover:text-blue-700 uppy:bg-transparent uppy:transition-colors"
|
||||
onClick={() => {
|
||||
editFile(file)
|
||||
}}
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="uppy:flex uppy:rounded uppy:text-blue-500 uppy:hover:text-blue-700 uppy:bg-transparent uppy:transition-colors"
|
||||
onClick={() => {
|
||||
ctx.uppy?.removeFile(file.id)
|
||||
}}
|
||||
>
|
||||
remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
packages/@uppy/components/src/FilesList.tsx
Normal file
93
packages/@uppy/components/src/FilesList.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
import { Fragment, h } from 'preact'
|
||||
import { useState, useEffect } from 'preact/hooks'
|
||||
|
||||
import type { Meta, Body, UppyEventMap, UppyFile } from '@uppy/core'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { clsx } from 'clsx'
|
||||
import { Thumbnail, type UppyContext } from './index.js'
|
||||
|
||||
export type FilesListProps = {
|
||||
editFile?: (file: UppyFile<Meta, Body>) => void
|
||||
ctx: UppyContext
|
||||
}
|
||||
|
||||
export default function FilesList(props: FilesListProps) {
|
||||
const [files, setFiles] = useState<UppyFile<any, any>[]>(() => [])
|
||||
const { ctx, editFile } = props
|
||||
|
||||
useEffect(() => {
|
||||
const onStateUpdate: UppyEventMap<any, any>['state-update'] = (
|
||||
prev,
|
||||
next,
|
||||
patch,
|
||||
) => {
|
||||
if (patch?.files) {
|
||||
setFiles(Object.values(patch.files))
|
||||
}
|
||||
}
|
||||
ctx.uppy?.on('state-update', onStateUpdate)
|
||||
return () => {
|
||||
ctx.uppy?.off('state-update', onStateUpdate)
|
||||
}
|
||||
}, [ctx.uppy])
|
||||
|
||||
return (
|
||||
<ul data-uppy-element="files-list" className="uppy-reset uppy:my-4">
|
||||
{files?.map((file) => (
|
||||
<li key={file.id}>
|
||||
<Fragment>
|
||||
<div className="uppy:flex uppy:items-center uppy:gap-2">
|
||||
<div className="uppy:w-[32px] uppy:h-[32px]">
|
||||
<Thumbnail width="32px" height="32px" file={file} />
|
||||
</div>
|
||||
|
||||
<p className="uppy:truncate">{file.name}</p>
|
||||
<p className="uppy:text-gray-500 uppy:tabular-nums uppy:min-w-18 uppy:text-right uppy:ml-auto">
|
||||
{prettyBytes(file.size || 0)}
|
||||
</p>
|
||||
|
||||
<Fragment>
|
||||
{editFile && (
|
||||
<button
|
||||
type="button"
|
||||
className="uppy:flex uppy:rounded uppy:text-blue-500 uppy:hover:text-blue-700 uppy:bg-transparent uppy:transition-colors"
|
||||
onClick={() => {
|
||||
editFile(file)
|
||||
}}
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="uppy:flex uppy:rounded uppy:text-blue-500 uppy:hover:text-blue-700 uppy:bg-transparent uppy:transition-colors"
|
||||
onClick={() => {
|
||||
ctx.uppy?.removeFile(file.id)
|
||||
}}
|
||||
>
|
||||
remove
|
||||
</button>
|
||||
</Fragment>
|
||||
</div>
|
||||
<progress
|
||||
max="100"
|
||||
className={clsx(
|
||||
'uppy:w-full uppy:h-[2px] uppy:appearance-none uppy:bg-gray-100 uppy:rounded-full uppy:overflow-hidden uppy:[&::-webkit-progress-bar]:bg-gray-100 uppy:block uppy:my-2',
|
||||
{
|
||||
'uppy:[&::-webkit-progress-value]:bg-green-500 uppy:[&::-moz-progress-bar]:bg-green-500':
|
||||
file.progress?.uploadComplete,
|
||||
'uppy:[&::-webkit-progress-value]:bg-red-500 uppy:[&::-moz-progress-bar]:bg-red-500':
|
||||
file.error,
|
||||
'uppy:[&::-webkit-progress-value]:bg-blue-500 uppy:[&::-moz-progress-bar]:bg-blue-500':
|
||||
!file.progress?.uploadComplete && !file.error,
|
||||
},
|
||||
)}
|
||||
value={file.progress?.percentage || 0}
|
||||
/>
|
||||
</Fragment>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
231
packages/@uppy/components/src/ProviderIcon.tsx
Normal file
231
packages/@uppy/components/src/ProviderIcon.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
import { h } from 'preact'
|
||||
import type { UppyContext } from './types'
|
||||
|
||||
export type ProviderIconProps = {
|
||||
provider:
|
||||
| 'device'
|
||||
| 'camera'
|
||||
| 'screen-capture'
|
||||
| 'audio'
|
||||
| 'dropbox'
|
||||
| 'facebook'
|
||||
| 'instagram'
|
||||
| 'onedrive'
|
||||
| 'googlephotos'
|
||||
| 'googledrive'
|
||||
fill?: string
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
ctx?: UppyContext
|
||||
}
|
||||
|
||||
export default function ProviderIcon(props: ProviderIconProps) {
|
||||
switch (props.provider) {
|
||||
case 'device':
|
||||
return (
|
||||
<svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M8.45 22.087l-1.305-6.674h17.678l-1.572 6.674H8.45zm4.975-12.412l1.083 1.765a.823.823 0 00.715.386h7.951V13.5H8.587V9.675h4.838zM26.043 13.5h-1.195v-2.598c0-.463-.336-.75-.798-.75h-8.356l-1.082-1.766A.823.823 0 0013.897 8H7.728c-.462 0-.815.256-.815.718V13.5h-.956a.97.97 0 00-.746.37.972.972 0 00-.19.81l1.724 8.565c.095.44.484.755.933.755H24c.44 0 .824-.3.929-.727l2.043-8.568a.972.972 0 00-.176-.825.967.967 0 00-.753-.38z"
|
||||
fill-rule="evenodd"
|
||||
fill={props.fill || 'currentcolor'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'camera':
|
||||
return (
|
||||
<svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M23.5 9.5c1.417 0 2.5 1.083 2.5 2.5v9.167c0 1.416-1.083 2.5-2.5 2.5h-15c-1.417 0-2.5-1.084-2.5-2.5V12c0-1.417 1.083-2.5 2.5-2.5h2.917l1.416-2.167C13 7.167 13.25 7 13.5 7h5c.25 0 .5.167.667.333L20.583 9.5H23.5zM16 11.417a4.706 4.706 0 00-4.75 4.75 4.704 4.704 0 004.75 4.75 4.703 4.703 0 004.75-4.75c0-2.663-2.09-4.75-4.75-4.75zm0 7.825c-1.744 0-3.076-1.332-3.076-3.074 0-1.745 1.333-3.077 3.076-3.077 1.744 0 3.074 1.333 3.074 3.076s-1.33 3.075-3.074 3.075z"
|
||||
fill-rule="nonzero"
|
||||
fill={props.fill || '#02B383'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'screen-capture':
|
||||
return (
|
||||
<svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g fill="currentcolor" fill-rule="evenodd">
|
||||
<path d="M24.182 9H7.818C6.81 9 6 9.742 6 10.667v10c0 .916.81 1.666 1.818 1.666h4.546V24h7.272v-1.667h4.546c1 0 1.809-.75 1.809-1.666l.009-10C26 9.742 25.182 9 24.182 9zM24 21H8V11h16v10z" />
|
||||
<circle cx="16" cy="16" r="2" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
case 'audio':
|
||||
return (
|
||||
<svg aria-hidden="true" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M21.143 12.297c.473 0 .857.383.857.857v2.572c0 3.016-2.24 5.513-5.143 5.931v2.64h2.572a.857.857 0 110 1.714H12.57a.857.857 0 110-1.714h2.572v-2.64C12.24 21.24 10 18.742 10 15.726v-2.572a.857.857 0 111.714 0v2.572A4.29 4.29 0 0016 20.01a4.29 4.29 0 004.286-4.285v-2.572c0-.474.384-.857.857-.857zM16 6.5a3 3 0 013 3v6a3 3 0 01-6 0v-6a3 3 0 013-3z"
|
||||
fill="currentcolor"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'dropbox':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
d="M10.5 7.5L5 10.955l5.5 3.454 5.5-3.454 5.5 3.454 5.5-3.454L21.5 7.5 16 10.955zM10.5 21.319L5 17.864l5.5-3.455 5.5 3.455zM16 17.864l5.5-3.455 5.5 3.455-5.5 3.455zM16 25.925l-5.5-3.455 5.5-3.454 5.5 3.454z"
|
||||
fill={props.fill || 'currentcolor'}
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'facebook':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M27 16c0-6.075-4.925-11-11-11S5 9.925 5 16c0 5.49 4.023 10.041 9.281 10.866V19.18h-2.793V16h2.793v-2.423c0-2.757 1.642-4.28 4.155-4.28 1.204 0 2.462.215 2.462.215v2.707h-1.387c-1.366 0-1.792.848-1.792 1.718V16h3.05l-.487 3.18h-2.563v7.686C22.977 26.041 27 21.49 27 16"
|
||||
fill="#1777F2"
|
||||
/>
|
||||
<path
|
||||
d="M20.282 19.18L20.77 16h-3.051v-2.063c0-.87.426-1.718 1.792-1.718h1.387V9.512s-1.258-.215-2.462-.215c-2.513 0-4.155 1.523-4.155 4.28V16h-2.793v3.18h2.793v7.686a11.082 11.082 0 003.438 0V19.18h2.563"
|
||||
fill="#FFFFFE"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
case 'instagram':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M16.825 5l.483-.001.799.002c1.168.005 1.598.021 2.407.057 1.17.05 1.97.235 2.67.506.725.28 1.34.655 1.951 1.265.613.61.99 1.223 1.273 1.946.273.7.46 1.498.516 2.67l.025.552.008.205c.029.748.037 1.51.042 3.777l.001.846v.703l-.001.398a50.82 50.82 0 01-.058 2.588c-.05 1.17-.235 1.97-.506 2.67a5.394 5.394 0 01-1.265 1.951c-.61.613-1.222.99-1.946 1.273-.699.273-1.498.46-2.668.516-.243.012-.451.022-.656.03l-.204.007c-.719.026-1.512.034-3.676.038l-.847.001h-1.1a50.279 50.279 0 01-2.587-.059c-1.171-.05-1.971-.235-2.671-.506a5.394 5.394 0 01-1.951-1.265 5.385 5.385 0 01-1.272-1.946c-.274-.699-.46-1.498-.517-2.668a88.15 88.15 0 01-.03-.656l-.007-.205c-.026-.718-.034-1.512-.038-3.674v-2.129c.006-1.168.022-1.597.058-2.406.051-1.171.235-1.971.506-2.672a5.39 5.39 0 011.265-1.95 5.381 5.381 0 011.946-1.272c.699-.274 1.498-.462 2.669-.517l.656-.03.204-.007c.718-.026 1.511-.034 3.674-.038zm.678 1.981h-1.226l-.295.001c-2.307.005-3.016.013-3.777.043l-.21.009-.457.02c-1.072.052-1.654.232-2.042.383-.513.2-.879.44-1.263.825a3.413 3.413 0 00-.82 1.267c-.15.388-.33.97-.375 2.043a48.89 48.89 0 00-.056 2.482v.398 1.565c.006 2.937.018 3.285.073 4.444.05 1.073.231 1.654.382 2.043.2.512.44.878.825 1.263.386.383.753.621 1.267.82.388.15.97.328 2.043.374.207.01.388.017.563.024l.208.007a63.28 63.28 0 002.109.026h1.564c2.938-.006 3.286-.019 4.446-.073 1.071-.051 1.654-.232 2.04-.383.514-.2.88-.44 1.264-.825.384-.386.622-.753.82-1.266.15-.389.328-.971.375-2.044.039-.88.054-1.292.057-2.723v-1.15-.572c-.006-2.936-.019-3.284-.074-4.445-.05-1.071-.23-1.654-.382-2.04-.2-.515-.44-.88-.825-1.264a3.405 3.405 0 00-1.267-.82c-.388-.15-.97-.328-2.042-.375a48.987 48.987 0 00-2.535-.056zm-1.515 3.37a5.65 5.65 0 11.021 11.299 5.65 5.65 0 01-.02-11.3zm.004 1.982a3.667 3.667 0 10.015 7.334 3.667 3.667 0 00-.015-7.334zm5.865-3.536a1.32 1.32 0 11.005 2.64 1.32 1.32 0 01-.005-2.64z"
|
||||
id="a"
|
||||
/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="b" fill="#fff">
|
||||
<use xlinkHref="#a" />
|
||||
</mask>
|
||||
<image
|
||||
mask="url(#b)"
|
||||
x="4"
|
||||
y="4"
|
||||
width="24"
|
||||
height="24"
|
||||
xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAALKADAAQAAAABAAAALAAAAAD8buejAAALZklEQVRYCVWZC2LbNhAFCRKykvP0bD1506SxRKIzbwHJoU3jv5h9WICU3P7+6zlG2zZvr8s/rW1tN7U0rMll8aDYufdzbLfc1JHmpv3jpPy8tsO+3O2s/O6YMSjTl/qdCds4mIIG60m8vdq2Z+phm2V4vAb9+o7BbZeuoM0NyYazvTvbvlN1MGjHUAesZ/IWWOsCeF0BOwAK4ITR0WYd/QKHEPv2DEymmorZtiubjOHEMYEzXmC9GMxu+95Kz+kuwxjDBKb8iUoCAdqZoAeyALreW6ZNx9Y4Jz8cLwjTZOEoR+HU05k2RzgP2iafGgfZiEdZbEr94zpX/xkPtDtGAxF+LRcgTsp9CAZg0rnEnXmPqFshY5vLnVWxLXO/bah2sZQgBZppGSe8NbjNPN5kc/WbIYEn8U+jXCOezT4zfgS1eoVEhceVeK74Fe4N6CoYEoLWykzHsd+GMAUqdTTVvvqT1uWqB3lVCLb12/ORAe8/5Zu9mp7lqoEFUCAFDIxqz7i1bq2AY1U9jqq2QK/7DYl+1AeZlAFcEc+U/jkRUqsvCHQ/nyGvjrOl6EuZWRWVGCKUMCkntQ5o+u2AZ3OxakbTcoBZnY0xhgGCUM4Kp1xtBTnBnXM5ASRms/Fs7d9OpX8bXN45pibQY/ML1MmA5G9CINBuCpdftexr6i2c5qd9J441LNJm3zk1GVusJ7v6mPJ7HPxJR0Li/vg9O1XHTEgvsQoSgExU0NnlLF0paK+6d06aOMKE2nCKV0ofNw4WsWmLsWrv6lPLnhGpr9E137QkHOMB/jh/T8MOqOadXarR44zPBW5NvDccnBxVmdK81+7RQ5p6qnQoRDZPh9+xWj0N2XpqxX1HzMty9UlFnKya/h3gulziAsyxwkSmpTIPB8vagKLyktRdDuBEHNGZMm4oCFWgjq31WPHpaC93gGNqpOpP4Ez4spa+nMNvhTWcuPKAJ79fqIxVoUvdjEG9qSy2WhpQlz61yG/gnKEA25IrIOYK6DIsQs2EE9LR/sTKq38Nd1y/X//FXG0QDHkEqSz3EYVV2dhb00rgLPSDcqmrScs55NNOD2zVqKmYnYTFnkACp520dkW5vBxK99BVzr792/iZ+VVo92UkKU2oG5WFTb6mNiA1H2C8KC0E44qaQleR3EQvQNwLrECOVAiSwM5gpF7nvDND0lZvYuQ9JbZfqdTrqCgwMcVrRS0z9QkLu9NWmkgEHb8p2zDRylj9VWA3lXD2vObEdWpT3w5MiFqQ1W/lteG4eipastxv2w+TeTBP0ypK84HiOW9fUzLcjRDwCW2b2VxmnGSKTX6uRSwMnC9YX4l05Mh2uwI+QVWdWUOSTWd5Xjjf7/tPYk2stSh053XTGN5RJMCMSajMcS8Trn3j/E1ajthlxCkmJXVi47PSUsyyq+jyexsayQNuv5GVYJaszprNsQD3RkgYiy49kFl2JlJJxlf8Uu/lpkq7+aWqzEzjr5cTVpFaJvSVr8AKRtiTlVPFk5t1nO30W+o6jrbAk76kxFa/tX+dom4C1wDPk03gqCw8HTBSxx4FHxIA+mh2pM3rKu5SNqBAuOSZnHzsB9JwW7DV/ge8dlVsOh375PvH8YO8EALU1HuecIC6qQgXifNuSx9XAoLaoGIYDjkWFrawX1U1XrknuMFw7QBSPtg79XovmBvwqnDICrhClEO6wgKFj9vPqJWlthUvdgH1DOA8+wFMexzQc5BUS1d1IsdBSjEv4Fe1LgBO1CpFPTpV1JuPSFNt4y/trzbtaUfwBWwM3/6JsrL6MSQYwLKXAm9YJBxsM8992MblZ63Gami0+rnwOMyPykVpQsyl9eYNOfVC6kRBkwaop//LgcAKWivkHF791g0JK5kMmCgKPas2QRkUFQsuTvm6R1946Wg95k764ZRLW59yO5UVGsawwELupCfAbdCuAwvcz5Xk18rIVEdgSRBRgO77R206QdXHuA2goaGiCQ0GmUfN1JlmFayjv0IcKGkfYt4HAj0yuQBRGDjzuS/rTmAf29Gov1S+FF7QBayNcpoBOEsMt3vFcIUC7VxOnE+pxmkgqEzduzwsPykrjBszCusgdarsRIAL6CM/KqsqcAf1vj8P1TXFyN6e5G8ao48fjKfDQJYizIdIfb+Xwp6Z2fE2C7mUfUEzMKqSBp4VUV1A49Sz1M2LzVzahEfyHUAcQNltR0nADYkBvHXDZQo8H9dQvHF7qhjPtSolBJ0A/vaLwdRz5YFFGoWBy8E/4aKcjqimaUBXXnjBpzOZnMlIVXsTVEBBUa+dD0BR0xVopgAD70psY0KjMHpmHB2kApea9o23NS83mpsref5OZet4U/0CMhSEDpwnxB9lVKSfk5djllXRFPizQmKcqMpnyZ3ycPntf96Ym9ChzU8vCQnhgWZ2UuySArw+cVBG4gqNCS6YoSEEziRWVStKUpe4FfCd91V0XA/qgOJuF7FpGjjyQgsFoNDtibp8cm+cyXxbB6zh4pMUO4H06yzsv4E/A6rg/uRJRnMRmrhMDIhyOjABX9CMDFhBFxx19KujjqWeim5PwVFU6IBiewfyk7IPETcg52kjXN7nsbaoEykKf/cjUgVxpTZZVtnqFMgv4FHa8oSOisawinMLHfUBzJcK1j8BeqquedKDtgcgnA4bym4P6gBWYVM3W/pn41ku5L4RElFWtlk5SXHEThhOWDiIyVROlQNM+wyHimlgATI/PPIm4BB8qfqwHnhgL89gzs+Ww1xQb4821SZ/4IwOJiRqH/X9u7Hj08JLSZfawOQcpRzwgk1oBNzzcgLn1FBNHspMENik9OG4awIDaUjw9rKNT1KXPl9neua6sSbkgqfs/CNfBdNfDDhQuL4AKXEXeOgZID91eOiRUnEFOIA5rnTkBU0/IT05gByoq5KBJF4Hym4Pxh3UcxZ7HjdhEhKWURbhavNR9rjLBwk3ryDcrGzfvk9I69b1yhMGWQ4bqMwv/RMSplQkjjVKXzZX8wESVcuB7QG0YUCMjk/aOmWgc/vC4oMCVYfghIGP6MT1zpeUhM1rQzOnGxmFKwTCir1Xaj5vN7T7nDZvnbDGHbCKnwji2zofNsOvbold3zlUtKGosBun3PbJSrrReHEaCQVCIDEMaCCBs+P+AbybkbIhmbNecGwF+E5/L2ECuPKCWsUESQkKnyyJ93TGACk7OrAY9P8XG//fGCoM7DAEUGnj5Mw7aQfelySWOm9iPuFyvrL8rKQR6mM6qdCUDQsfNPVu4yv/HaPOT1e/yDaviMKmTkg/I/F7MUG9OlrmDrBLRVd3c8KBJlPEKoVRcIJuhoQAmZDUkPC00W5OI1dOpQ1F61kFNqr9SmFcaHdBheOaDHF6QZMOP6QyiZ804oj98wLiAMIgcWw4UDYkDAWfR+4d5s0zP2GgUZX04i+NeSgYGokvbDhIZYUWHgd9K8zZzir264NxZUFbsfM1jdqpV2naA48tx6hsvBSabE4IMtlcOGgq8PqCjoly2rw2soqy4RJWQtPZl6PUCU14ZUWENuZV2Honn3f+k6R6wrkqgTStyQ0bFY+XAaafMRFgUlVeXxXFUcpLEYfZz3FrVUzZrOOJK+4B/wnIZ8TGRvb9OB8EUM0w8uNYj/oa9iK9AMoy6gA72o02srMxpAPUD+EDnVEF7P5xw896VyAbFk8MgnpVpR3gfLnt/wECq3rYFvYLcKCpqvcI+/hVl8AumXDeApklDRRKJSS+KOaq1Rgg4igOYtiQK1hJy46TBtDjznDp3iqJff5j0/LfSZbYVdauqXccJ9W+czupp0sU9gMlqkQ52lU1E6tUwoDUukAD6YRpAwqDrAErzA8QCRvXm98KEep0xIdY1CN1ye27IP0IHvvYIW18qGz8S7VWUZuMkUOb3P8DHTl67ur/i1UAAAAASUVORK5CYII="
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
case 'onedrive':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<g fill="none" fillRule="nonzero">
|
||||
<path
|
||||
d="M13.39 12.888l4.618 2.747 2.752-1.15a4.478 4.478 0 012.073-.352 6.858 6.858 0 00-5.527-5.04 6.895 6.895 0 00-6.876 2.982l.07-.002a5.5 5.5 0 012.89.815z"
|
||||
fill="#0364B8"
|
||||
/>
|
||||
<path
|
||||
d="M13.39 12.887v.001a5.5 5.5 0 00-2.89-.815l-.07.002a5.502 5.502 0 00-4.822 2.964 5.43 5.43 0 00.38 5.62l4.073-1.702 1.81-.757 4.032-1.685 2.105-.88-4.619-2.748z"
|
||||
fill="#0078D4"
|
||||
/>
|
||||
<path
|
||||
d="M22.833 14.133a4.479 4.479 0 00-2.073.352l-2.752 1.15.798.475 2.616 1.556 1.141.68 3.902 2.321a4.413 4.413 0 00-.022-4.25 4.471 4.471 0 00-3.61-2.284z"
|
||||
fill="#1490DF"
|
||||
/>
|
||||
<path
|
||||
d="M22.563 18.346l-1.141-.68-2.616-1.556-.798-.475-2.105.88L11.87 18.2l-1.81.757-4.073 1.702A5.503 5.503 0 0010.5 23h12.031a4.472 4.472 0 003.934-2.333l-3.902-2.321z"
|
||||
fill="#28A8EA"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
case 'googlephotos':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="-7 -7 73 73"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M-3-3h64v64H-3z" />
|
||||
<g fillRule="nonzero">
|
||||
<path
|
||||
fill="#FBBC04"
|
||||
d="M14.8 13.4c8.1 0 14.7 6.6 14.7 14.8v1.3H1.3c-.7 0-1.3-.6-1.3-1.3C0 20 6.6 13.4 14.8 13.4z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M45.6 14.8c0 8.1-6.6 14.7-14.8 14.7h-1.3V1.3c0-.7.6-1.3 1.3-1.3C39 0 45.6 6.6 45.6 14.8z"
|
||||
/>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M44.3 45.6c-8.2 0-14.8-6.6-14.8-14.8v-1.3h28.2c.7 0 1.3.6 1.3 1.3 0 8.2-6.6 14.8-14.8 14.8z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M13.4 44.3c0-8.2 6.6-14.8 14.8-14.8h1.3v28.2c0 .7-.6 1.3-1.3 1.3-8.2 0-14.8-6.6-14.8-14.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
case 'googledrive':
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<g fillRule="nonzero" fill="none">
|
||||
<path
|
||||
d="M6.663 22.284l.97 1.62c.202.34.492.609.832.804l3.465-5.798H5c0 .378.1.755.302 1.096l1.361 2.278z"
|
||||
fill="#0066DA"
|
||||
/>
|
||||
<path
|
||||
d="M16 12.09l-3.465-5.798c-.34.195-.63.463-.832.804l-6.4 10.718A2.15 2.15 0 005 18.91h6.93L16 12.09z"
|
||||
fill="#00AC47"
|
||||
/>
|
||||
<path
|
||||
d="M23.535 24.708c.34-.195.63-.463.832-.804l.403-.67 1.928-3.228c.201-.34.302-.718.302-1.096h-6.93l1.474 2.802 1.991 2.996z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
<path
|
||||
d="M16 12.09l3.465-5.798A2.274 2.274 0 0018.331 6h-4.662c-.403 0-.794.11-1.134.292L16 12.09z"
|
||||
fill="#00832D"
|
||||
/>
|
||||
<path
|
||||
d="M20.07 18.91h-8.14l-3.465 5.798c.34.195.73.292 1.134.292h12.802c.403 0 .794-.11 1.134-.292L20.07 18.91z"
|
||||
fill="#2684FC"
|
||||
/>
|
||||
<path
|
||||
d="M23.497 12.455l-3.2-5.359a2.252 2.252 0 00-.832-.804L16 12.09l4.07 6.82h6.917c0-.377-.1-.755-.302-1.096l-3.188-5.359z"
|
||||
fill="#FFBA00"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
182
packages/@uppy/components/src/Thumbnail.tsx
Normal file
182
packages/@uppy/components/src/Thumbnail.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
import { h } from 'preact'
|
||||
import { useMemo, useEffect } from 'preact/hooks'
|
||||
import type { Body, Meta, UppyFile } from '@uppy/core'
|
||||
import type { UppyContext } from './types'
|
||||
|
||||
export type ThumbnailProps = {
|
||||
file: UppyFile<Meta, Body>
|
||||
width?: string
|
||||
height?: string
|
||||
images?: boolean
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
ctx?: UppyContext
|
||||
}
|
||||
|
||||
export default function Thumbnail(props: ThumbnailProps) {
|
||||
const width = props.width || '100%'
|
||||
const height = props.height || '100%'
|
||||
const fileTypeGeneral = props.file.type?.split('/')[0] || ''
|
||||
const fileTypeSpecific = props.file.type?.split('/')[1] || ''
|
||||
const isImage = props.file.type.startsWith('image/')
|
||||
const isArchive =
|
||||
fileTypeGeneral === 'application' &&
|
||||
[
|
||||
'zip',
|
||||
'x-7z-compressed',
|
||||
'x-zip-compressed',
|
||||
'x-rar-compressed',
|
||||
'x-tar',
|
||||
'x-gzip',
|
||||
'x-apple-diskimage',
|
||||
].includes(fileTypeSpecific)
|
||||
const isPDF = fileTypeGeneral === 'application' && fileTypeSpecific === 'pdf'
|
||||
|
||||
const objectUrl = useMemo(() => {
|
||||
if (!props.images) return ''
|
||||
return URL.createObjectURL(props.file.data)
|
||||
}, [props.file.data, props.images])
|
||||
const showThumbnail = props.images && isImage && objectUrl
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}
|
||||
}, [objectUrl])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-uppy-element="thumbnail"
|
||||
className="uppy:relative uppy:overflow-hidden uppy:bg-gray-100 uppy:rounded-lg uppy:flex uppy:items-center uppy:justify-center"
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
aspectRatio: '1',
|
||||
}}
|
||||
>
|
||||
{showThumbnail ?
|
||||
<img
|
||||
className="uppy:w-full uppy:h-full uppy:object-cover"
|
||||
src={objectUrl}
|
||||
alt={props.file.name}
|
||||
/>
|
||||
: null}
|
||||
{!showThumbnail ?
|
||||
<div className="uppy:flex uppy:flex-col uppy:items-center uppy:justify-center uppy:w-full uppy:h-full">
|
||||
<div className="uppy:flex-1 uppy:flex uppy:items-center uppy:justify-center uppy:w-full">
|
||||
{!props.file.type ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<g fill="#A7AFB7" fill-rule="nonzero">
|
||||
<path d="M5.5 22a.5.5 0 0 1-.5-.5v-18a.5.5 0 0 1 .5-.5h10.719a.5.5 0 0 1 .367.16l3.281 3.556a.5.5 0 0 1 .133.339V21.5a.5.5 0 0 1-.5.5h-14zm.5-1h13V7.25L16 4H6v17z" />
|
||||
<path d="M15 4v3a1 1 0 0 0 1 1h3V7h-3V4h-1z" />
|
||||
</g>
|
||||
</svg>
|
||||
: null}
|
||||
{fileTypeGeneral === 'text' ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<path
|
||||
d="M4.5 7h13a.5.5 0 1 1 0 1h-13a.5.5 0 0 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h10a.5.5 0 1 1 0 1h-10a.5.5 0 1 1 0-1z"
|
||||
fill="#5A5E69"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
: null}
|
||||
{fileTypeGeneral === 'image' && !showThumbnail ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<g fill="#686DE0" fill-rule="evenodd">
|
||||
<path
|
||||
d="M5 7v10h15V7H5zm0-1h15a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
<path
|
||||
d="M6.35 17.172l4.994-5.026a.5.5 0 0 1 .707 0l2.16 2.16 3.505-3.505a.5.5 0 0 1 .707 0l2.336 2.31-.707.72-1.983-1.97-3.505 3.505a.5.5 0 0 1-.707 0l-2.16-2.159-3.938 3.939-1.409.026z"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
<circle cx="7.5" cy="9.5" r="1.5" />
|
||||
</g>
|
||||
</svg>
|
||||
: null}
|
||||
{fileTypeGeneral === 'audio' ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<path
|
||||
d="M9.5 18.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V7.25a.5.5 0 0 1 .379-.485l9-2.25A.5.5 0 0 1 18.5 5v11.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V8.67l-8 2v7.97zm8-11v-2l-8 2v2l8-2zM7 19.64c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1zm9-2c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1z"
|
||||
fill="#049BCF"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
: null}
|
||||
{fileTypeGeneral === 'video' ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<path
|
||||
d="M16 11.834l4.486-2.691A1 1 0 0 1 22 10v6a1 1 0 0 1-1.514.857L16 14.167V17a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2.834zM15 9H5v8h10V9zm1 4l5 3v-6l-5 3z"
|
||||
fill="#19AF67"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
: null}
|
||||
{isPDF ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<path
|
||||
d="M9.766 8.295c-.691-1.843-.539-3.401.747-3.726 1.643-.414 2.505.938 2.39 3.299-.039.79-.194 1.662-.537 3.148.324.49.66.967 1.055 1.51.17.231.382.488.629.757 1.866-.128 3.653.114 4.918.655 1.487.635 2.192 1.685 1.614 2.84-.566 1.133-1.839 1.084-3.416.249-1.141-.604-2.457-1.634-3.51-2.707a13.467 13.467 0 0 0-2.238.426c-1.392 4.051-4.534 6.453-5.707 4.572-.986-1.58 1.38-4.206 4.914-5.375.097-.322.185-.656.264-1.001.08-.353.306-1.31.407-1.737-.678-1.059-1.2-2.031-1.53-2.91zm2.098 4.87c-.033.144-.068.287-.104.427l.033-.01-.012.038a14.065 14.065 0 0 1 1.02-.197l-.032-.033.052-.004a7.902 7.902 0 0 1-.208-.271c-.197-.27-.38-.526-.555-.775l-.006.028-.002-.003c-.076.323-.148.632-.186.8zm5.77 2.978c1.143.605 1.832.632 2.054.187.26-.519-.087-1.034-1.113-1.473-.911-.39-2.175-.608-3.55-.608.845.766 1.787 1.459 2.609 1.894zM6.559 18.789c.14.223.693.16 1.425-.413.827-.648 1.61-1.747 2.208-3.206-2.563 1.064-4.102 2.867-3.633 3.62zm5.345-10.97c.088-1.793-.351-2.48-1.146-2.28-.473.119-.564 1.05-.056 2.405.213.566.52 1.188.908 1.859.18-.858.268-1.453.294-1.984z"
|
||||
fill="#E2514A"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
: null}
|
||||
{isArchive ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<path
|
||||
d="M10.45 2.05h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V2.55a.5.5 0 0 1 .5-.5zm2.05 1.024h1.05a.5.5 0 0 1 .5.5V3.6a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5v-.001zM10.45 0h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V.5a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 3.074h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 1.024h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm-2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm-2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-1.656 3.074l-.82 5.946c.52.302 1.174.458 1.976.458.803 0 1.455-.156 1.975-.458l-.82-5.946h-2.311zm0-1.025h2.312c.512 0 .946.378 1.015.885l.82 5.946c.056.412-.142.817-.501 1.026-.686.398-1.515.597-2.49.597-.974 0-1.804-.199-2.49-.597a1.025 1.025 0 0 1-.5-1.026l.819-5.946c.07-.507.503-.885 1.015-.885zm.545 6.6a.5.5 0 0 1-.397-.561l.143-.999a.5.5 0 0 1 .495-.429h.74a.5.5 0 0 1 .495.43l.143.998a.5.5 0 0 1-.397.561c-.404.08-.819.08-1.222 0z"
|
||||
fill="#00C469"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
: null}
|
||||
{props.file.type && !fileTypeGeneral && !isPDF && !isArchive ?
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="uppy:w-3/4 uppy:h-3/4"
|
||||
viewBox="0 0 25 25"
|
||||
>
|
||||
<g fill="#A7AFB7" fill-rule="nonzero">
|
||||
<path d="M5.5 22a.5.5 0 0 1-.5-.5v-18a.5.5 0 0 1 .5-.5h10.719a.5.5 0 0 1 .367.16l3.281 3.556a.5.5 0 0 1 .133.339V21.5a.5.5 0 0 1-.5.5h-14zm.5-1h13V7.25L16 4H6v17z" />
|
||||
<path d="M15 4v3a1 1 0 0 0 1 1h3V7h-3V4h-1z" />
|
||||
</g>
|
||||
</svg>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
packages/@uppy/components/src/UploadButton.tsx
Normal file
105
packages/@uppy/components/src/UploadButton.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import { h } from 'preact'
|
||||
import { clsx } from 'clsx'
|
||||
import type { UppyContext } from './types'
|
||||
|
||||
export type UploadButtonProps = {
|
||||
ctx: UppyContext
|
||||
}
|
||||
|
||||
export default function UploadButton(props: UploadButtonProps) {
|
||||
const { ctx } = props
|
||||
|
||||
return (
|
||||
<div className="uppy-reset uppy:space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
data-uppy-element="upload-button"
|
||||
data-state={ctx.status}
|
||||
onClick={() => {
|
||||
if (ctx.status === 'ready') {
|
||||
ctx.uppy?.upload()
|
||||
}
|
||||
}}
|
||||
className={clsx(
|
||||
'uppy:relative uppy:w-full uppy:p-2 uppy:rounded-lg',
|
||||
'uppy:text-white uppy:font-medium',
|
||||
'uppy:transition-all uppy:overflow-hidden',
|
||||
'uppy:bg-blue-500 uppy:hover:bg-blue-600',
|
||||
{
|
||||
'uppy:bg-red-500 uppy:hover:bg-red-600': ctx.status === 'error',
|
||||
'uppy:bg-green-500 uppy:hover:bg-green-600':
|
||||
ctx.status === 'complete',
|
||||
},
|
||||
'uppy:disabled:hover:bg-blue-500 uppy:disabled:cursor-not-allowed',
|
||||
)}
|
||||
disabled={
|
||||
ctx.status === 'init' ||
|
||||
ctx.status === 'uploading' ||
|
||||
ctx.status === 'paused'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'uppy:absolute uppy:inset-0 uppy:origin-left uppy:transition-all',
|
||||
{
|
||||
'uppy:bg-red-700': ctx.status === 'error',
|
||||
'uppy:bg-green-700': ctx.status === 'complete',
|
||||
'uppy:bg-blue-700':
|
||||
ctx.status !== 'error' && ctx.status !== 'complete',
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
transform: `scaleX(${ctx.progress / 100})`,
|
||||
}}
|
||||
/>
|
||||
<span className="uppy:relative uppy:z-10">
|
||||
{ctx.status === 'uploading' || ctx.status === 'paused' ?
|
||||
`Uploaded ${Math.round(ctx.progress)}%`
|
||||
: ctx.status === 'error' ?
|
||||
'Retry'
|
||||
: ctx.status === 'complete' ?
|
||||
'Complete'
|
||||
: 'Upload'}
|
||||
</span>
|
||||
</button>
|
||||
{ctx.status === 'uploading' || ctx.status === 'paused' ?
|
||||
<div className="uppy:flex uppy:gap-2">
|
||||
{ctx.uppy?.getState().capabilities.resumableUploads ?
|
||||
<button
|
||||
type="button"
|
||||
data-uppy-element="pause-button"
|
||||
data-state={ctx.status}
|
||||
onClick={() => {
|
||||
if (ctx.status === 'paused') {
|
||||
ctx.uppy?.resumeAll()
|
||||
} else {
|
||||
ctx.uppy?.pauseAll()
|
||||
}
|
||||
}}
|
||||
className={clsx(
|
||||
'uppy:w-full uppy:p-2 uppy:rounded-lg uppy:text-amber-500 uppy:bg-gray-50 uppy:hover:bg-amber-50 uppy:font-medium uppy:transition-all',
|
||||
{
|
||||
'uppy:text-green-500 uppy:hover:bg-green-50':
|
||||
ctx.status === 'paused',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{ctx.status === 'paused' ? 'Resume' : 'Pause'}
|
||||
</button>
|
||||
: null}
|
||||
<button
|
||||
type="button"
|
||||
data-uppy-element="cancel-button"
|
||||
className="uppy:w-full uppy:p-2 uppy:rounded-lg uppy:text-red-500 uppy:bg-gray-50 uppy:hover:bg-red-50 uppy:font-medium uppy:transition-all"
|
||||
data-state={ctx.status}
|
||||
onClick={() => ctx.uppy?.cancelAll()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
packages/@uppy/components/src/index.ts
Normal file
17
packages/@uppy/components/src/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export { default as Thumbnail, type ThumbnailProps } from './Thumbnail.js'
|
||||
export { default as FilesList, type FilesListProps } from './FilesList.js'
|
||||
export { default as FilesGrid, type FilesGridProps } from './FilesGrid.js'
|
||||
export { default as Dropzone, type DropzoneProps } from './Dropzone.js'
|
||||
export {
|
||||
default as UploadButton,
|
||||
type UploadButtonProps,
|
||||
} from './UploadButton.js'
|
||||
export {
|
||||
default as ProviderIcon,
|
||||
type ProviderIconProps,
|
||||
} from './ProviderIcon.js'
|
||||
|
||||
export type { UppyContext } from './types.js'
|
||||
|
||||
export { createUppyEventAdapter } from './uppyEventAdapter.js'
|
||||
export type { UploadStatus } from './types.js'
|
||||
66
packages/@uppy/components/src/input.css
Normal file
66
packages/@uppy/components/src/input.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
@layer theme, base, components, utilities;
|
||||
|
||||
/*
|
||||
* Split up tailwind imports to not import the preflight styles
|
||||
* as we don't want our components to affect the entire website of the user.
|
||||
*/
|
||||
@import 'tailwindcss/theme.css' layer(theme) prefix(uppy);
|
||||
@import 'tailwindcss/utilities.css' layer(utilities) prefix(uppy);
|
||||
|
||||
@import '@uppy/core/dist/style.min.css';
|
||||
@import '@uppy/image-editor/dist/style.min.css';
|
||||
@import '@uppy/webcam/dist/style.min.css';
|
||||
@import '@uppy/audio/dist/style.min.css';
|
||||
@import '@uppy/screen-capture/dist/style.min.css';
|
||||
|
||||
/*
|
||||
* Instead we took the preflight styles from tailwind and apply it under a .uppy-reset class
|
||||
* so we can enjoy the benefits of preflight styles without affecting the rest of the website.
|
||||
* Put under a @layer to prevent specificity issues.
|
||||
*/
|
||||
@layer components {
|
||||
.uppy-reset,
|
||||
.uppy-reset * {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
|
||||
.uppy-reset {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.uppy-reset h1,
|
||||
.uppy-reset h2,
|
||||
.uppy-reset h3,
|
||||
.uppy-reset h4,
|
||||
.uppy-reset h5,
|
||||
.uppy-reset h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.uppy-reset ol,
|
||||
.uppy-reset ul,
|
||||
.uppy-reset menu {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.uppy-reset img,
|
||||
.uppy-reset video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.uppy-reset button,
|
||||
.uppy-reset input,
|
||||
.uppy-reset select,
|
||||
.uppy-reset textarea {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
15
packages/@uppy/components/src/types.ts
Normal file
15
packages/@uppy/components/src/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type Uppy from '@uppy/core'
|
||||
|
||||
export type UploadStatus =
|
||||
| 'init'
|
||||
| 'ready'
|
||||
| 'uploading'
|
||||
| 'paused'
|
||||
| 'error'
|
||||
| 'complete'
|
||||
|
||||
export type UppyContext = {
|
||||
uppy: Uppy | undefined
|
||||
status: UploadStatus
|
||||
progress: number
|
||||
}
|
||||
62
packages/@uppy/components/src/uppyEventAdapter.ts
Normal file
62
packages/@uppy/components/src/uppyEventAdapter.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Uppy from '@uppy/core'
|
||||
import type { UploadStatus } from './types'
|
||||
|
||||
export function createUppyEventAdapter({
|
||||
uppy,
|
||||
onStatusChange,
|
||||
onProgressChange,
|
||||
}: {
|
||||
uppy: Uppy
|
||||
onStatusChange: (status: UploadStatus) => void
|
||||
onProgressChange: (progress: number) => void
|
||||
}): { cleanup: () => void } {
|
||||
const onFileAdded = () => {
|
||||
onStatusChange('ready')
|
||||
}
|
||||
const onUploadStarted = () => {
|
||||
onStatusChange('uploading')
|
||||
}
|
||||
const onResumeAll = () => {
|
||||
onStatusChange('uploading')
|
||||
}
|
||||
const onComplete = () => {
|
||||
onStatusChange('complete')
|
||||
onProgressChange(0)
|
||||
}
|
||||
const onError = () => {
|
||||
onStatusChange('error')
|
||||
onProgressChange(0)
|
||||
}
|
||||
const onCancelAll = () => {
|
||||
onStatusChange('init')
|
||||
onProgressChange(0)
|
||||
}
|
||||
const onPauseAll = () => {
|
||||
onStatusChange('paused')
|
||||
}
|
||||
const onProgress = (p: number) => {
|
||||
onProgressChange(p)
|
||||
}
|
||||
|
||||
uppy.on('file-added', onFileAdded)
|
||||
uppy.on('progress', onProgress)
|
||||
uppy.on('upload', onUploadStarted)
|
||||
uppy.on('complete', onComplete)
|
||||
uppy.on('error', onError)
|
||||
uppy.on('cancel-all', onCancelAll)
|
||||
uppy.on('pause-all', onPauseAll)
|
||||
uppy.on('resume-all', onResumeAll)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
uppy.off('file-added', onFileAdded)
|
||||
uppy.off('progress', onProgress)
|
||||
uppy.off('upload', onUploadStarted)
|
||||
uppy.off('complete', onComplete)
|
||||
uppy.off('error', onError)
|
||||
uppy.off('cancel-all', onCancelAll)
|
||||
uppy.off('pause-all', onPauseAll)
|
||||
uppy.off('resume-all', onResumeAll)
|
||||
},
|
||||
}
|
||||
}
|
||||
30
packages/@uppy/components/tsconfig.build.json
Normal file
30
packages/@uppy/components/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"resolveJsonModule": false,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../audio/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../google-drive-picker/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../image-editor/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
packages/@uppy/components/tsconfig.json
Normal file
28
packages/@uppy/components/tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../audio/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../google-drive-picker/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../image-editor/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import { render } from 'preact'
|
||||
import { render } from 'preact/compat'
|
||||
import findDOMElement from '@uppy/utils/lib/findDOMElement'
|
||||
import getTextDirection from '@uppy/utils/lib/getTextDirection'
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@
|
|||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uppy/components": "workspace:^",
|
||||
"@uppy/utils": "workspace:^",
|
||||
"preact": "^10.5.13",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -38,8 +40,11 @@
|
|||
"@uppy/drag-drop": "workspace:^",
|
||||
"@uppy/file-input": "workspace:^",
|
||||
"@uppy/progress-bar": "workspace:^",
|
||||
"@uppy/screen-capture": "workspace:^",
|
||||
"@uppy/status-bar": "workspace:^",
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
"@uppy/webcam": "workspace:^",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@uppy/dashboard": {
|
||||
|
|
@ -54,8 +59,14 @@
|
|||
"@uppy/progress-bar": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/screen-capture": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/status-bar": {
|
||||
"optional": true
|
||||
},
|
||||
"@uppy/webcam": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
packages/@uppy/react/src/headless/Dropzone.tsx
Normal file
27
packages/@uppy/react/src/headless/Dropzone.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
type DropzoneProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function Dropzone(props: Omit<DropzoneProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactDropzone, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies DropzoneProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
27
packages/@uppy/react/src/headless/FilesGrid.tsx
Normal file
27
packages/@uppy/react/src/headless/FilesGrid.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
type FilesGridProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function FilesGrid(props: Omit<FilesGridProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactFilesGrid, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesGridProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
27
packages/@uppy/react/src/headless/FilesList.tsx
Normal file
27
packages/@uppy/react/src/headless/FilesList.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
type FilesListProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function FilesList(props: Omit<FilesListProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactFilesList, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesListProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
27
packages/@uppy/react/src/headless/ProviderIcon.tsx
Normal file
27
packages/@uppy/react/src/headless/ProviderIcon.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
type ProviderIconProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function ProviderIcon(props: Omit<ProviderIconProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactProviderIcon, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ProviderIconProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
27
packages/@uppy/react/src/headless/Thumbnail.tsx
Normal file
27
packages/@uppy/react/src/headless/Thumbnail.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
type ThumbnailProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function Thumbnail(props: Omit<ThumbnailProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactThumbnail, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ThumbnailProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
27
packages/@uppy/react/src/headless/UploadButton.tsx
Normal file
27
packages/@uppy/react/src/headless/UploadButton.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
type UploadButtonProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
|
||||
export default function UploadButton(props: Omit<UploadButtonProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
preactRender(
|
||||
preactH(PreactUploadButton, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies UploadButtonProps),
|
||||
ref.current,
|
||||
)
|
||||
}
|
||||
}, [ctx, props])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
64
packages/@uppy/react/src/headless/UppyContextProvider.tsx
Normal file
64
packages/@uppy/react/src/headless/UppyContextProvider.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useEffect,
|
||||
createElement as h,
|
||||
} from 'react'
|
||||
import type Uppy from '@uppy/core'
|
||||
import { createUppyEventAdapter, type UploadStatus } from '@uppy/components'
|
||||
|
||||
interface UppyContextValue {
|
||||
uppy: Uppy | undefined
|
||||
status: UploadStatus
|
||||
progress: number
|
||||
}
|
||||
|
||||
export const UppyContext = createContext<UppyContextValue>({
|
||||
uppy: undefined,
|
||||
status: 'init',
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
interface Props {
|
||||
uppy: Uppy
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function UppyContextProvider({ uppy, children }: Props) {
|
||||
const [status, setStatus] = useState<UploadStatus>('init')
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!uppy) {
|
||||
throw new Error(
|
||||
'UppyContextProvider: passing `uppy` as a prop is required',
|
||||
)
|
||||
}
|
||||
|
||||
const uppyEventAdapter = createUppyEventAdapter({
|
||||
uppy,
|
||||
onStatusChange: (newStatus: UploadStatus) => {
|
||||
setStatus(newStatus)
|
||||
},
|
||||
onProgressChange: (newProgress: number) => {
|
||||
setProgress(newProgress)
|
||||
},
|
||||
})
|
||||
|
||||
return () => uppyEventAdapter.cleanup()
|
||||
}, [uppy])
|
||||
|
||||
return (
|
||||
<UppyContext.Provider
|
||||
value={{
|
||||
uppy,
|
||||
status,
|
||||
progress,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UppyContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default UppyContextProvider
|
||||
6
packages/@uppy/react/src/headless/index.ts
Normal file
6
packages/@uppy/react/src/headless/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { default as Dropzone } from './Dropzone.js'
|
||||
export { default as FilesGrid } from './FilesGrid.js'
|
||||
export { default as FilesList } from './FilesList.js'
|
||||
export { default as ProviderIcon } from './ProviderIcon.js'
|
||||
export { default as Thumbnail } from './Thumbnail.js'
|
||||
export { default as UploadButton } from './UploadButton.js'
|
||||
|
|
@ -6,3 +6,10 @@ export { default as StatusBar } from './StatusBar.js'
|
|||
export { default as FileInput } from './FileInput.js'
|
||||
export { default as useUppyState } from './useUppyState.js'
|
||||
export { default as useUppyEvent } from './useUppyEvent.js'
|
||||
|
||||
// Headless components
|
||||
export {
|
||||
UppyContext,
|
||||
UppyContextProvider,
|
||||
} from './headless/UppyContextProvider.js'
|
||||
export * from './headless/index.js'
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../components/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../dashboard/tsconfig.build.json"
|
||||
},
|
||||
|
|
@ -30,6 +33,12 @@
|
|||
},
|
||||
{
|
||||
"path": "../status-bar/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../screen-capture/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../webcam/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../components/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../dashboard/tsconfig.build.json",
|
||||
},
|
||||
|
|
@ -29,5 +32,11 @@
|
|||
{
|
||||
"path": "../status-bar/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../screen-capture/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../webcam/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
1
packages/@uppy/svelte/.gitignore
vendored
1
packages/@uppy/svelte/.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
|||
.DS_Store
|
||||
/dist/
|
||||
/src/empty.*
|
||||
.svelte-kit
|
||||
!src/lib
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./dist/styles.css": "./dist/styles.css",
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"svelte": "./dist/index.js"
|
||||
|
|
@ -35,16 +36,20 @@
|
|||
"prepublishOnly": "yarn run package",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uppy/components": "workspace:^",
|
||||
"preact": "^10.26.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
"@sveltejs/package": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.4.17"
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
"@sveltejs/package": "^2.3.11",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"svelte": "^5.27.0",
|
||||
"svelte-check": "^4.1.6",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "workspace:^",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
type DropzoneProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<DropzoneProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactDropzone, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies DropzoneProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
type FilesGridProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<FilesGridProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactFilesGrid, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesGridProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
type FilesListProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<FilesListProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactFilesList, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesListProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
type ProviderIconProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<ProviderIconProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactProviderIcon, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ProviderIconProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
type ThumbnailProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<ThumbnailProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactThumbnail, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ThumbnailProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
type UploadButtonProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<UploadButtonProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
let container: HTMLElement
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
preactRender(
|
||||
preactH(PreactUploadButton, {
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies UploadButtonProps),
|
||||
container,
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<script context="module" lang="ts">
|
||||
import type Uppy from '@uppy/core';
|
||||
import { createUppyEventAdapter, type UploadStatus } from '@uppy/components'
|
||||
|
||||
export const UppyContextKey = 'uppy-context';
|
||||
export type { UppyContext } from '@uppy/components';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext, onMount } from 'svelte';
|
||||
|
||||
export let uppy: Uppy;
|
||||
|
||||
let status: UploadStatus = 'init';
|
||||
let progress = 0;
|
||||
|
||||
onMount(() => {
|
||||
if (!uppy) {
|
||||
throw new Error(
|
||||
'ContextProvider: passing `uppy` as a prop is required',
|
||||
);
|
||||
}
|
||||
|
||||
const uppyEventAdapter = createUppyEventAdapter({
|
||||
uppy,
|
||||
onStatusChange: (newStatus: UploadStatus) => {
|
||||
status = newStatus
|
||||
},
|
||||
onProgressChange: (newProgress: number) => {
|
||||
progress = newProgress
|
||||
},
|
||||
})
|
||||
|
||||
return () => uppyEventAdapter.cleanup()
|
||||
});
|
||||
|
||||
// Create a reactive store from our context values
|
||||
$: contextValue = {
|
||||
uppy,
|
||||
status,
|
||||
progress,
|
||||
};
|
||||
|
||||
// Set the context for child components to use
|
||||
$: setContext(UppyContextKey, contextValue);
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export { default as Dropzone } from './Dropzone.svelte'
|
||||
export { default as FilesGrid } from './FilesGrid.svelte'
|
||||
export { default as FilesList } from './FilesList.svelte'
|
||||
export { default as ProviderIcon } from './ProviderIcon.svelte'
|
||||
export { default as Thumbnail } from './Thumbnail.svelte'
|
||||
export { default as UploadButton } from './UploadButton.svelte'
|
||||
|
|
@ -3,3 +3,7 @@ export { default as DashboardModal } from './components/DashboardModal.svelte'
|
|||
export { default as DragDrop } from './components/DragDrop.svelte'
|
||||
export { default as ProgressBar } from './components/ProgressBar.svelte'
|
||||
export { default as StatusBar } from './components/StatusBar.svelte'
|
||||
|
||||
// Headless components
|
||||
export { default as UppyContextProvider } from './components/headless/UppyContextProvider.svelte'
|
||||
export * from './components/headless/index.js'
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import adapter from '@sveltejs/adapter-auto'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// eslint-disable-next-line import/no-unresolved
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { defineConfig } from 'vite'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@uppy/components": "workspace:^",
|
||||
"preact": "^10.5.13",
|
||||
"shallow-equal": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
48
packages/@uppy/vue/src/headless/Dropzone.ts
Normal file
48
packages/@uppy/vue/src/headless/Dropzone.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
type DropzoneProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<DropzoneProps, 'ctx'>>({
|
||||
name: 'Dropzone',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderDropzone() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactDropzone, {
|
||||
...(attrs as DropzoneProps),
|
||||
ctx,
|
||||
} satisfies DropzoneProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderDropzone()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderDropzone()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderDropzone()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
48
packages/@uppy/vue/src/headless/FilesGrid.ts
Normal file
48
packages/@uppy/vue/src/headless/FilesGrid.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
type FilesGridProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesGridProps, 'ctx'>>({
|
||||
name: 'FilesGrid',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderFilesGrid() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactFilesGrid, {
|
||||
...(attrs as FilesGridProps),
|
||||
ctx,
|
||||
} satisfies FilesGridProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderFilesGrid()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderFilesGrid()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderFilesGrid()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
48
packages/@uppy/vue/src/headless/FilesList.ts
Normal file
48
packages/@uppy/vue/src/headless/FilesList.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
type FilesListProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesListProps, 'ctx'>>({
|
||||
name: 'FilesList',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderFilesList() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactFilesList, {
|
||||
...(attrs as FilesListProps),
|
||||
ctx,
|
||||
} satisfies FilesListProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderFilesList()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderFilesList()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderFilesList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
48
packages/@uppy/vue/src/headless/ProviderIcon.ts
Normal file
48
packages/@uppy/vue/src/headless/ProviderIcon.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
type ProviderIconProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ProviderIconProps, 'ctx'>>({
|
||||
name: 'ProviderIcon',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderProviderIcon() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactProviderIcon, {
|
||||
...(attrs as ProviderIconProps),
|
||||
ctx,
|
||||
} satisfies ProviderIconProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderProviderIcon()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderProviderIcon()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderProviderIcon()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
48
packages/@uppy/vue/src/headless/Thumbnail.ts
Normal file
48
packages/@uppy/vue/src/headless/Thumbnail.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
type ThumbnailProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ThumbnailProps, 'ctx'>>({
|
||||
name: 'Thumbnail',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderThumbnail() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactThumbnail, {
|
||||
...(attrs as ThumbnailProps),
|
||||
ctx,
|
||||
} satisfies ThumbnailProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderThumbnail()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderThumbnail()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderThumbnail()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
48
packages/@uppy/vue/src/headless/UploadButton.ts
Normal file
48
packages/@uppy/vue/src/headless/UploadButton.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
type UploadButtonProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<UploadButtonProps, 'ctx'>>({
|
||||
name: 'UploadButton',
|
||||
setup(props, { attrs }) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
function renderUploadButton() {
|
||||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactUploadButton, {
|
||||
...(attrs as UploadButtonProps),
|
||||
ctx,
|
||||
} satisfies UploadButtonProps),
|
||||
containerRef.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderUploadButton()
|
||||
})
|
||||
|
||||
watch(ctx, () => {
|
||||
renderUploadButton()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderUploadButton()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
},
|
||||
})
|
||||
73
packages/@uppy/vue/src/headless/context-provider.ts
Normal file
73
packages/@uppy/vue/src/headless/context-provider.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
defineComponent,
|
||||
provide,
|
||||
reactive,
|
||||
ref,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type Uppy from '@uppy/core'
|
||||
import { createUppyEventAdapter } from '@uppy/components'
|
||||
import type { UploadStatus } from '@uppy/components'
|
||||
|
||||
export interface UppyContext {
|
||||
uppy: Uppy | undefined
|
||||
status: UploadStatus
|
||||
progress: number
|
||||
}
|
||||
|
||||
export const UppyContextSymbol = Symbol('uppy')
|
||||
|
||||
export const UppyContextProvider = defineComponent({
|
||||
name: 'UppyContextProvider',
|
||||
props: {
|
||||
uppy: {
|
||||
type: Object as PropType<Uppy>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const status = ref<UploadStatus>('init')
|
||||
const progress = ref(0)
|
||||
|
||||
const uppyContext = reactive<UppyContext>({
|
||||
uppy: props.uppy,
|
||||
status: 'init',
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
// Provide the context immediately instead of in onMounted
|
||||
provide(UppyContextSymbol, uppyContext)
|
||||
|
||||
let uppyEventAdapter: ReturnType<typeof createUppyEventAdapter> | undefined
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.uppy) {
|
||||
throw new Error(
|
||||
'UppyContextProvider: passing `uppy` as a prop is required',
|
||||
)
|
||||
}
|
||||
|
||||
uppyEventAdapter = createUppyEventAdapter({
|
||||
uppy: props.uppy,
|
||||
onStatusChange: (newStatus: UploadStatus) => {
|
||||
status.value = newStatus
|
||||
uppyContext.status = newStatus
|
||||
},
|
||||
onProgressChange: (newProgress: number) => {
|
||||
progress.value = newProgress
|
||||
uppyContext.progress = newProgress
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uppyEventAdapter?.cleanup()
|
||||
})
|
||||
|
||||
return () => slots.default?.()
|
||||
},
|
||||
})
|
||||
|
||||
export default UppyContextProvider
|
||||
6
packages/@uppy/vue/src/headless/index.ts
Normal file
6
packages/@uppy/vue/src/headless/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { default as Dropzone } from './Dropzone.js'
|
||||
export { default as FilesGrid } from './FilesGrid.js'
|
||||
export { default as FilesList } from './FilesList.js'
|
||||
export { default as ProviderIcon } from './ProviderIcon.js'
|
||||
export { default as Thumbnail } from './Thumbnail.js'
|
||||
export { default as UploadButton } from './UploadButton.js'
|
||||
16
packages/@uppy/vue/src/headless/useUppyContext.ts
Normal file
16
packages/@uppy/vue/src/headless/useUppyContext.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { inject } from 'vue'
|
||||
import { UppyContextSymbol, type UppyContext } from './context-provider.js'
|
||||
|
||||
export function useUppyContext(): UppyContext {
|
||||
const context = inject<UppyContext>(UppyContextSymbol)
|
||||
|
||||
if (!context) {
|
||||
return {
|
||||
uppy: undefined,
|
||||
status: 'init',
|
||||
progress: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue