Appearance
@vibe-labs/design-vue-uploads
Vue 3 file upload components for the Vibe Design System. Drop zone, file list with previews, upload queue with concurrency control, validation, auto-retry, and cancellation.
Installation
ts
import { VibeUploadZone, VibeUploadList, VibeUploadItem, VibeUploadIndicator, useUploadQueue } from "@vibe-labs/design-vue-uploads";Requires the CSS layer from @vibe-labs/design-components-uploads.
Quick Start
vue
<script setup>
import { VibeUploadZone, VibeUploadList, VibeUploadItem, useUploadQueue } from "@vibe-labs/design-vue-uploads";
const { files, addFiles, removeFile, retryFile, cancelFile, progress, isUploading } = useUploadQueue({
upload: async (file, { onProgress, signal }) => {
const form = new FormData();
form.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: form, signal });
return res.json(); // → stored as file.meta
},
concurrency: 3,
validation: {
accept: ["image/*", ".pdf"],
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 20,
},
onValidationError: (errors) => toast.warning(errors[0].message),
});
</script>
<template>
<VibeUploadZone accept="image/*,.pdf" @files="addFiles">
<template #hint>Max 10MB per file · Images and PDFs</template>
</VibeUploadZone>
<VibeUploadList>
<VibeUploadItem v-for="f in files" :key="f.id" :file="f" @remove="removeFile" @retry="retryFile" @cancel="cancelFile" />
</VibeUploadList>
</template>Components
VibeUploadZone
Drop zone with drag-and-drop, click-to-browse, and directory upload support.
Usage
vue
<!-- Basic -->
<VibeUploadZone @files="addFiles" />
<!-- With accept filter -->
<VibeUploadZone accept="image/*,.pdf" @files="addFiles" />
<!-- Single file -->
<VibeUploadZone :multiple="false" @files="addFiles" />
<!-- Compact layout -->
<VibeUploadZone compact @files="addFiles" />
<!-- Directory upload -->
<VibeUploadZone directory @files="addFiles" />
<!-- Disabled -->
<VibeUploadZone disabled />
<!-- Custom content -->
<VibeUploadZone @files="addFiles" v-slot="{ isDragOver, open }">
<div :class="{ 'highlight': isDragOver }">
<p>Drop files here</p>
<button @click="open">Or click to browse</button>
</div>
</VibeUploadZone>
<!-- Custom label and hint -->
<VibeUploadZone @files="addFiles">
<template #label>Upload your documents</template>
<template #hint>PDF, DOC, up to 25MB</template>
</VibeUploadZone>Props
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string | — | Accepted file types (HTML accept attribute) |
multiple | boolean | true | Allow multiple files |
disabled | boolean | false | Disabled state |
compact | boolean | false | Compact horizontal layout |
directory | boolean | false | Allow directory upload (webkitdirectory) |
Events
| Event | Payload | Description |
|---|---|---|
files | FileList | Files selected or dropped |
Slots
| Slot | Props | Description |
|---|---|---|
default | { isDragOver, open } | Full custom content |
icon | — | Custom upload icon |
label | — | Custom label text |
hint | — | Hint text below label |
Exposed
| Method | Description |
|---|---|
openFilePicker() | Programmatically open the file dialog |
VibeUploadList
Scrollable container for upload items.
vue
<VibeUploadList>
<VibeUploadItem v-for="f in files" :key="f.id" :file="f" />
</VibeUploadList>
<!-- Unbounded (no max-height) -->
<VibeUploadList unbounded>...</VibeUploadList>Props
| Prop | Type | Default | Description |
|---|---|---|---|
unbounded | boolean | false | Remove max-height constraint |
VibeUploadItem
Individual file row with thumbnail, progress bar, status icons, and actions.
Usage
vue
<VibeUploadItem :file="uploadFile" @remove="removeFile" @retry="retryFile" @cancel="cancelFile" />
<!-- Without preview -->
<VibeUploadItem :file="f" :show-preview="false" />
<!-- Non-removable -->
<VibeUploadItem :file="f" :removable="false" />
<!-- Custom thumbnail -->
<VibeUploadItem :file="f">
<template #thumb="{ file }">
<MyCustomThumb :file="file" />
</template>
</VibeUploadItem>Props
| Prop | Type | Default | Description |
|---|---|---|---|
file | UploadFile | required | The upload file object |
showPreview | boolean | true | Show thumbnail for images |
removable | boolean | true | Show remove/cancel button |
Events
| Event | Payload | Description |
|---|---|---|
remove | string (id) | Remove file |
retry | string (id) | Retry failed upload |
cancel | string (id) | Cancel in-progress upload |
Slots
| Slot | Props | Description |
|---|---|---|
thumb | { file } | Custom thumbnail |
file-icon | { file } | Icon for non-image files |
status | { status } | Custom status indicator |
VibeUploadIndicator
Compact status button showing upload progress — useful in toolbars or headers.
vue
<VibeUploadIndicator
:progress="progress"
:active="stats.uploading"
:total="stats.total"
:complete="isComplete"
:error="hasErrors"
@click="toggleUploadPanel"
/>
<!-- Custom label -->
<VibeUploadIndicator :progress="80" :active="2" :total="5" label="Uploading assets…" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
progress | number | required | Overall progress 0–100 |
active | number | required | Active upload count |
total | number | required | Total file count |
complete | boolean | false | All complete |
error | boolean | false | Any errors |
label | string | auto | Label text override |
Composable
useUploadQueue(options)
The core upload engine — manages the queue, concurrency, validation, retry, and cancellation.
ts
const queue = useUploadQueue({
upload: async (file, { onProgress, signal }) => {
// Your upload implementation
// Call onProgress(0–100) for progress updates
// Respect signal for cancellation
return serverResponse; // → stored as file.meta
},
concurrency: 3,
autoUpload: true,
maxRetries: 2,
previews: true,
validation: {
accept: ["image/*", ".pdf", ".docx"],
maxFileSize: 10 * 1024 * 1024,
maxFiles: 20,
},
onValidationError: (errors) => { /* handle invalid files */ },
onFileComplete: (file) => { /* individual file done */ },
onFileError: (file) => { /* individual file failed */ },
onQueueComplete: (files) => { /* all files done */ },
});Options
| Option | Type | Default | Description |
|---|---|---|---|
upload | UploadFn<TMeta> | required | Upload implementation |
concurrency | number | 3 | Max parallel uploads |
autoUpload | boolean | true | Start immediately when files added |
maxRetries | number | 0 | Auto-retry count on failure |
previews | boolean | true | Generate image preview URLs |
validation | UploadValidation | — | Validation rules |
onValidationError | (errors) => void | — | Validation failure callback |
onFileComplete | (file) => void | — | Per-file completion |
onFileError | (file) => void | — | Per-file error |
onQueueComplete | (files) => void | — | All files done |
Returns
| Property | Type | Description |
|---|---|---|
files | Ref<UploadFile[]> | All files in the queue |
addFiles | (files) => void | Add files (validates first) |
removeFile | (id) => void | Remove file (cancels if active) |
retryFile | (id) => void | Retry a failed file |
cancelFile | (id) => void | Cancel active upload (aborts request) |
startAll | () => void | Start all queued (when autoUpload is false) |
cancelAll | () => void | Cancel all active uploads |
clearCompleted | () => void | Remove completed files |
clearAll | () => void | Remove all files, cancel active |
progress | ComputedRef<number> | Overall progress 0–100 |
isUploading | ComputedRef<boolean> | Any uploads active |
isComplete | ComputedRef<boolean> | All files done |
hasErrors | ComputedRef<boolean> | Any errors |
stats | ComputedRef<UploadStats> | Aggregate counts by status |
Upload File Lifecycle
queued → pending → uploading → complete
↘ error → (retry) → queued
↘ cancelled (via abort)Validation
Runs before files enter the queue. Three checks in order:
- Count — total files vs
maxFiles - Type — MIME wildcards (
image/*), exact MIME, or extensions (.pdf) - Size — file bytes vs
maxFileSize
Invalid files are reported via onValidationError and never enter the queue.
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-uploads | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.
Usage Guide
Setup
ts
import {
VibeUploadZone,
VibeUploadList,
VibeUploadItem,
VibeUploadIndicator,
useUploadQueue,
} from "@vibe-labs/design-vue-uploads";
import "@vibe-labs/design-components-uploads";useUploadQueue + Components — Practical Examples
Image Gallery Upload
vue
<script setup lang="ts">
import { VibeUploadZone, VibeUploadList, VibeUploadItem, useUploadQueue } from "@vibe-labs/design-vue-uploads";
import { toast } from "@vibe-labs/design-vue-toasts";
interface ImageMeta { url: string; width: number; height: number }
const {
files,
addFiles,
removeFile,
retryFile,
cancelFile,
clearCompleted,
progress,
isUploading,
isComplete,
hasErrors,
stats,
} = useUploadQueue<ImageMeta>({
upload: async (file, { onProgress, signal }) => {
const form = new FormData();
form.append("image", file);
const res = await fetch("/api/images", { method: "POST", body: form, signal });
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json(); // returns { url, width, height }
},
concurrency: 3,
maxRetries: 2,
validation: {
accept: ["image/jpeg", "image/png", "image/webp"],
maxFileSize: 5 * 1024 * 1024, // 5MB
maxFiles: 50,
},
onValidationError: (errors) => {
errors.forEach((e) => toast.warning(e.message));
},
onQueueComplete: (completed) => {
toast.success({ title: `${completed.length} images uploaded` });
},
});
</script>
<template>
<div class="upload-panel">
<VibeUploadZone
accept="image/jpeg,image/png,image/webp"
@files="addFiles"
>
<template #label>Drop images here</template>
<template #hint>JPEG, PNG or WebP · Max 5MB each</template>
</VibeUploadZone>
<div v-if="files.length > 0" class="upload-actions">
<span>{{ stats.uploading }} uploading · {{ stats.complete }} complete</span>
<button v-if="isComplete" @click="clearCompleted">Clear all</button>
</div>
<VibeUploadList>
<VibeUploadItem
v-for="f in files"
:key="f.id"
:file="f"
@remove="removeFile"
@retry="retryFile"
@cancel="cancelFile"
/>
</VibeUploadList>
</div>
</template>Document Upload with Manual Start
vue
<script setup lang="ts">
import {
VibeUploadZone,
VibeUploadList,
VibeUploadItem,
useUploadQueue,
} from "@vibe-labs/design-vue-uploads";
const { files, addFiles, removeFile, retryFile, cancelFile, startAll, isUploading, progress } = useUploadQueue({
upload: async (file, { onProgress, signal }) => {
const form = new FormData();
form.append("document", file);
const res = await fetch("/api/documents", { method: "POST", body: form, signal });
return res.json();
},
autoUpload: false, // wait for explicit start
concurrency: 1, // sequential uploads for documents
validation: {
accept: [".pdf", ".docx", ".xlsx"],
maxFileSize: 25 * 1024 * 1024, // 25MB
maxFiles: 10,
},
});
</script>
<template>
<VibeUploadZone accept=".pdf,.docx,.xlsx" @files="addFiles">
<template #hint>PDF, Word, or Excel · Max 25MB</template>
</VibeUploadZone>
<VibeUploadList unbounded>
<VibeUploadItem
v-for="f in files"
:key="f.id"
:file="f"
@remove="removeFile"
@retry="retryFile"
@cancel="cancelFile"
/>
</VibeUploadList>
<div class="upload-footer">
<button
:disabled="files.length === 0 || isUploading"
@click="startAll"
>
Upload {{ files.length }} file(s)
</button>
</div>
</template>Toolbar Upload Indicator
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeUploadIndicator, useUploadQueue } from "@vibe-labs/design-vue-uploads";
const panelOpen = ref(false);
const { progress, isUploading, isComplete, hasErrors, stats, addFiles, files, removeFile, retryFile, cancelFile } = useUploadQueue({
upload: async (file, { onProgress, signal }) => {
/* your upload fn */
},
});
</script>
<template>
<header class="app-header">
<AppLogo />
<nav><!-- nav items --></nav>
<VibeUploadIndicator
:progress="progress"
:active="stats.uploading"
:total="stats.total"
:complete="isComplete"
:error="hasErrors"
@click="panelOpen = !panelOpen"
/>
</header>
<!-- Slide-out upload panel -->
<aside v-if="panelOpen" class="upload-drawer">
<VibeUploadList>
<VibeUploadItem
v-for="f in files"
:key="f.id"
:file="f"
@remove="removeFile"
@retry="retryFile"
@cancel="cancelFile"
/>
</VibeUploadList>
</aside>
</template>Common Patterns
Custom File Preview Slot
vue
<template>
<VibeUploadItem :file="f" @remove="removeFile">
<template #thumb="{ file }">
<!-- Show PDF icon instead of image preview -->
<PdfIcon v-if="file.file.type === 'application/pdf'" />
<img v-else :src="file.previewUrl" :alt="file.file.name" />
</template>
</VibeUploadItem>
</template>Programmatic Drop Zone Trigger
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeUploadZone } from "@vibe-labs/design-vue-uploads";
const zoneRef = ref();
function triggerPicker() {
zoneRef.value?.openFilePicker();
}
</script>
<template>
<VibeUploadZone ref="zoneRef" @files="addFiles" />
<button @click="triggerPicker">Choose files</button>
</template>Callbacks for Per-File Lifecycle Events
ts
useUploadQueue({
upload: myUploadFn,
onFileComplete: (file) => {
// Access server response via file.meta
attachedFiles.push(file.meta.url);
},
onFileError: (file) => {
console.error(`Failed: ${file.file.name}`, file.error);
},
onQueueComplete: () => {
router.push("/review");
},
});