Skip to content

@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

PropTypeDefaultDescription
acceptstringAccepted file types (HTML accept attribute)
multiplebooleantrueAllow multiple files
disabledbooleanfalseDisabled state
compactbooleanfalseCompact horizontal layout
directorybooleanfalseAllow directory upload (webkitdirectory)

Events

EventPayloadDescription
filesFileListFiles selected or dropped

Slots

SlotPropsDescription
default{ isDragOver, open }Full custom content
iconCustom upload icon
labelCustom label text
hintHint text below label

Exposed

MethodDescription
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

PropTypeDefaultDescription
unboundedbooleanfalseRemove 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

PropTypeDefaultDescription
fileUploadFilerequiredThe upload file object
showPreviewbooleantrueShow thumbnail for images
removablebooleantrueShow remove/cancel button

Events

EventPayloadDescription
removestring (id)Remove file
retrystring (id)Retry failed upload
cancelstring (id)Cancel in-progress upload

Slots

SlotPropsDescription
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

PropTypeDefaultDescription
progressnumberrequiredOverall progress 0–100
activenumberrequiredActive upload count
totalnumberrequiredTotal file count
completebooleanfalseAll complete
errorbooleanfalseAny errors
labelstringautoLabel 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

OptionTypeDefaultDescription
uploadUploadFn<TMeta>requiredUpload implementation
concurrencynumber3Max parallel uploads
autoUploadbooleantrueStart immediately when files added
maxRetriesnumber0Auto-retry count on failure
previewsbooleantrueGenerate image preview URLs
validationUploadValidationValidation rules
onValidationError(errors) => voidValidation failure callback
onFileComplete(file) => voidPer-file completion
onFileError(file) => voidPer-file error
onQueueComplete(files) => voidAll files done

Returns

PropertyTypeDescription
filesRef<UploadFile[]>All files in the queue
addFiles(files) => voidAdd files (validates first)
removeFile(id) => voidRemove file (cancels if active)
retryFile(id) => voidRetry a failed file
cancelFile(id) => voidCancel active upload (aborts request)
startAll() => voidStart all queued (when autoUpload is false)
cancelAll() => voidCancel all active uploads
clearCompleted() => voidRemove completed files
clearAll() => voidRemove all files, cancel active
progressComputedRef<number>Overall progress 0–100
isUploadingComputedRef<boolean>Any uploads active
isCompleteComputedRef<boolean>All files done
hasErrorsComputedRef<boolean>Any errors
statsComputedRef<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:

  1. Count — total files vs maxFiles
  2. Type — MIME wildcards (image/*), exact MIME, or extensions (.pdf)
  3. Size — file bytes vs maxFileSize

Invalid files are reported via onValidationError and never enter the queue.


Dependencies

PackagePurpose
@vibe-labs/design-components-uploadsCSS tokens + generated styles

Build

bash
npm run build

Built 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

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");
  },
});

Vibe