npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ilhooq/upload-manager

v0.2.0

Published

A lightweight, framework-agnostic file upload manager widget built with Svelte 5 and Uppy.

Readme

Upload Manager

A lightweight file-uploader widget for the web — a modern, framework-agnostic alternative to Blueimp's jQuery File Upload. Built with Svelte 5 (compiled to a custom element) on top of Uppy, and shipped as ready-to-use ES/UMD bundles.

It provides drag-and-drop uploads, a file table with previews and progress bars, reordering, per-file captions, and a list of files already stored on the server — all driven by a simple HTTP contract you implement on the backend.

UI

Features

  • Drag-and-drop or file-picker uploads (single or multiple files).
  • Live progress bars and per-file status.
  • Image previews (thumbnails) for local and remote files.
  • Remote file listing: shows files already stored on the server alongside pending uploads.
  • Reordering (move up / down) for both local and remote files, with optional server persistence.
  • Editable per-file captions persisted to the server.
  • File size restriction.
  • Built-in i18n (English default, French included), custom locale packs, and per-key label overrides.
  • Event callbacks for every lifecycle step.
  • No framework required on the host page — it registers a <upload-manager> custom element.

Installation

Install from npm:

npm install @ilhooq/upload-manager

For local development in this repository:

npm install
npm run build   # produces dist/ (ES + UMD bundles + style.css)

The build outputs:

| File | Description | | --- | --- | | dist/upload-manager.es.js | ES module bundle (import) | | dist/upload-manager.umd.js | UMD bundle (require / <script>) | | dist/upload-manager.css | Stylesheet (import or <link>) |

package.json exposes these through the exports map, so consumers can do:

import { UploaderWidget } from "@ilhooq/upload-manager"
import "@ilhooq/upload-manager/style.css"

Quick start

<link rel="stylesheet" href="/dist/upload-manager.css">
<div id="uploader"></div>

<script type="module">
  import { UploaderWidget } from "/dist/upload-manager.es.js"

  const widget = new UploaderWidget("#uploader", {
    endpoint: "/api/files",        // POST  — upload (multipart)
    listEndpoint: "/api/files",    // GET   — list existing files
    updateEndpoint: "/api/files",  // PATCH — update caption
    orderEndpoint: "/api/files",   // POST  — persist full ordering ({ order: [ids] })
    deleteEndpoint: "/api/files",  // DELETE?id=... — remove a file
    fieldName: "file",
    maxFileSize: 5 * 1024 * 1024,
    locale: "en",
    onUploadSuccess: ({ file, serverId }) => {
      console.log("uploaded", file.name, "->", serverId)
    }
  })
</script>

All endpoints may point to the same URL and dispatch by HTTP method (the upload and the ordering both use POST, distinguished by Content-Type: multipart for upload, application/json for ordering — see the PHP reference in example/app.php).

Running the example

The repository ships with a complete PHP reference backend.

npm run backend   # PHP built-in server on localhost:8081 (serves example/app.php)
npm run dev       # Vite dev server, opens example/index-dev.html (uses src/ directly)

Both processes must run together: the Vite dev server proxies /example/app.php to the PHP server. index-dev.html imports the widget straight from source (no build step needed); index.html uses the built dist/ artifacts.

API

The library exposes two layers:

  • UploaderWidget — the public entry point. Mounts the UI into a DOM element, wires options to the core, and bridges events to callbacks. This is what most consumers use.
  • UploaderCore — the framework-agnostic engine that owns all state and network calls. Use it directly if you want to build your own UI or integrate with another framework.

Exports

import {
  UploaderWidget,           // public widget class
  UploaderCore,             // headless engine
  DEFAULT_OPTIONS,          // default option values
  DEFAULT_LABELS,           // default locale's string set
  resolveLocale,            // (locale, labels) => { strings, pluralize }
  formatBytes,              // (bytes) => human-readable string
  locales, DEFAULT_LOCALE,  // locale registry + default key ("en")
  fr, en                    // individual locale packs
} from "@ilhooq/upload-manager"

UploaderWidget is also assigned to window.UploaderWidget for non-module usage.


new UploaderWidget(target, options)

Creates and mounts the widget.

  • target — a CSS selector string or a DOM element. Throws if the target cannot be found.
  • options — see Options below. Callbacks may be passed either at the top level (onReady, onUploadSuccess, …) or nested under options.callbacks.

Construction immediately mounts the <upload-manager> element and calls core.init() (which loads remote files if showRemoteFiles is enabled).

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | endpoint | string | "./app.php" | URL for the POST upload request. | | listEndpoint | string | "./app.php" | URL for the GET listing of remote files. | | updateEndpoint | string | "./app.php" | URL for the PATCH update (caption). Falls back to orderEndpoint if not set. | | deleteEndpoint | string | "./app.php" | URL for the DELETE request (?id=<id>). | | orderEndpoint | string | "./app.php" | URL for the POST ordering request (full ordered list of ids). | | fieldName | string | "file" | Multipart form field name for the uploaded file. | | maxFileSize | number | 5242880 (5 MB) | Maximum size per file, in bytes. | | autoProceed | boolean | false | Start uploading as soon as files are added. | | multiple | boolean | true | Allow more than one file (false limits to 1). | | showRemoteFiles | boolean | true | Fetch and display files already on the server. | | persistOrder | boolean | true | Persist remote reordering to the server via POST to orderEndpoint. | | locale | string | object | "en" | Locale key ("en", "fr"), or a custom { strings, pluralize } pack. | | labels | object | {} | Per-key string overrides applied on top of the chosen locale. |

Methods

| Method | Description | | --- | --- | | getState() | Returns the current state object. | | addFiles(files) | Adds an array of File/Blob objects to the upload queue. | | startUpload() | Async. Uploads all pending files. | | cancelAll() | Cancels all in-progress uploads. | | reload() | Async. Re-fetches the remote file list. | | removeLocalById(id) | Async. Removes a local file by its Uppy id (deletes from server too if already uploaded). | | removeRemoteById(id) | Async. Deletes a remote file by its server id. | | destroy() | Tears down the widget: unsubscribes events, closes Uppy, revokes preview URLs, empties the target. |

Callbacks

Each callback receives a single object argument that always contains widget (the UploaderWidget instance) and state (the current state), plus event-specific fields.

| Callback | Payload (besides widget, state) | Fired when | | --- | --- | --- | | onReady | — | The core has initialized (remote files loaded). | | onChange | — | Any state change (progress, add, remove, reorder…). | | onUploadSuccess | file, response, serverId | A file finished uploading. | | onUploadError | file, error, response | An upload failed. | | onFileUpdate | id, changes, file | A PATCH caption update succeeded. | | onFileUpdateError | id, changes, error | A PATCH caption update failed. | | onReorder | scope ("local"/"remote"), id, direction ("up"/"down"), order, persisted, error? | A file was moved. | | onDeleteSuccess | id, scope | A file was removed. | | onDeleteError | id, scope, error | A removal failed. | | onErrorAddFile | file, error | Adding a file to the queue failed (e.g. rejected by Uppy — size/type restriction). If no callback is registered, the core logs the error to the console instead. |


new UploaderCore(options)

The headless engine. Accepts the same options as the widget (minus callbacks). Use it to drive a custom UI.

const core = new UploaderCore({ endpoint: "/api/files" })
const unsubscribe = core.subscribe((state) => render(state))
await core.init()

State subscription vs. event bus

The core exposes two distinct notification channels:

  • subscribe(listener) — calls listener(state) immediately and on every state change. Returns an unsubscribe function. Use it to render the full UI.
  • on(eventName, listener) — subscribes to a named domain event. Returns an unsubscribe function. Events: ready, change, uploadSuccess, uploadError, fileUpdate, fileUpdateError, reorder, deleteSuccess, deleteError, errorAddFile. (UploaderWidget bridges these to the on* callbacks above.) on(event, listener) and emit(event, payload) return early when there is no subscriber; emit returns true when at least one listener ran, false otherwise — the core uses this to fall back to console.error for errorAddFile when the host registered no handler.

Methods

| Method | Description | | --- | --- | | init() | Async. Loads remote files (if enabled), emits ready, notifies subscribers. | | getState() | Returns the current state object. | | subscribe(listener) | Subscribe to full-state changes. Returns unsubscribe fn. | | on(event, listener) | Subscribe to a named event. Returns unsubscribe fn. | | addFiles(files) | Add files to the Uppy queue. | | startUpload() | Async. Upload pending files. | | cancelAll() | Cancel all uploads. | | moveLocal(id, direction) | Reorder a local file ("up" / "down"). | | moveRemote(id, direction) | Async. Reorder a remote file; persists the full ordering via POST to orderEndpoint when persistOrder is on. Calls are serialized; on failure the list is resynced from the server. | | removeLocalById(id) | Async. Remove a local file (and its server copy if uploaded). | | removeRemoteById(id) | Async. Delete a remote file. | | saveRemoteCaption(id, caption) | Async. Persist a caption via PATCH (no-op if unchanged). | | reload() | Async. Re-fetch the remote file list. | | t(key, vars) | Translate a locale key with %{var} interpolation and pluralization. | | destroy() | Tear down: close Uppy, revoke previews, clear listeners. |

State object

getState() and every subscriber/callback receive:

{
  localFiles: [ /* ordered Uppy file objects (pending or uploading) */ ],
  remoteFiles: [ /* ordered remote file descriptors from the server */ ],
  counts: { local: Number, remote: Number },
  progress: Number   // 0–100, overall upload progress of local files
}

Local vs. remote files are two independent ordered lists. localFiles are files in Uppy; remoteFiles are fetched from listEndpoint. On upload success, a file is promoted into the remote list (when showRemoteFiles is on) so it renders identically to files already on the server.


Helpers

  • resolveLocale(locale, labels){ strings, pluralize }. Merges default pack → chosen pack → labels. Accepts a known key, a full/partial pack object.
  • formatBytes(bytes) → human-readable size string (e.g. "1.5 Mo"). Returns "-" for non-positive/invalid input.

Internationalization (i18n)

UI strings live in locale packs under src/locales/ (en.js, fr.js). Each pack is:

export default {
  pluralize: (n) => (n === 1 ? 0 : 1),
  strings: { addFiles: "Add files", /* … */ }
}

The locale option accepts:

  • a known key — locale: "fr"
  • a custom pack — locale: { strings: {...}, pluralize: (n) => ... }
  • a partial pack (missing keys fall back to the default locale)

The legacy labels option overrides individual keys on top of the resolved locale:

new UploaderWidget("#uploader", {
  locale: "fr",
  labels: { addFiles: "Choisir des fichiers" }
})

Plural strings are objects keyed by pluralize(count); pass count in vars when translating. Strings support %{var} interpolation (e.g. previewAlt: "Preview of %{name}").

Server contract

You implement five endpoints (which may share one URL, dispatching by HTTP method — and, for the two POST routes, by Content-Type). example/app.php is a complete reference implementation that persists ordering and captions in example/uploads/.meta.json and sanitizes/de-duplicates uploaded file names.

GET listEndpoint — list remote files

{
  "files": [
    {
      "id": "photo.jpg",
      "name": "photo.jpg",
      "size": 12345,
      "type": "image/jpeg",
      "url": "uploads/photo.jpg",
      "caption": "optional",
      "order": 1
    }
  ]
}

Recognized fields: id, name, size, type, url, caption?, order? (1-based), plus any extra scalar fields (e.g. thumbUrl, updatedAt).

POST endpoint — upload (multipart)

Field name from the fieldName option. Response:

{
  "id": "photo.jpg",
  "file": { "id": "photo.jpg", "name": "photo.jpg", "url": "uploads/photo.jpg", "...": "..." }
}

The server id extracted by the client is response.body.id or response.body.file.id.

PATCH updateEndpoint — update caption

Request body:

{ "id": "photo.jpg", "changes": { "caption": "New caption" } }

Response:

{ "id": "photo.jpg", "file": { "...": "..." } }

POST orderEndpoint — persist ordering (application/json)

Sent when persistOrder is on. The body carries the complete ordered list of ids (display order); the server rewrites each file's position accordingly. Ids missing from the list keep their relative order after the listed ones.

{ "order": ["photo.jpg", "scan.png", "report.pdf"] }

Any 2xx response is accepted; returning the refreshed list ({ "files": [...] }) is optional, as the client already applied the move optimistically.

DELETE deleteEndpoint?id=<id> — remove a file

{ "deleted": "photo.jpg" }

Non-2xx responses should return { "error": "message" }, which the client surfaces in the thrown error.

Architecture

Three layers, with strict boundaries:

  1. src/core/uploader-core.js (UploaderCore) — owns all state and side effects (Uppy instance, remote list, ordering, preview URLs, network requests). Framework-agnostic, no DOM.
  2. src/ui/UploaderElement.svelte — Svelte 5 component compiled to the <upload-manager> custom element (shadow: "none"). Receives the core via a core prop and re-renders on core.subscribe(...).
  3. src/adapters/widget.js (UploaderWidget) — public entry point. Wires options to the core, mounts the element, bridges core events to user callbacks, and registers the custom element tag once.

When changing behavior: state/network → core; rendering → Svelte component; option/callback shape → adapter.

Scripts

| Command | Description | | --- | --- | | npm run dev | Vite dev server (opens example/index-dev.html, uses src/). | | npm run backend | PHP built-in server on localhost:8081 serving example/app.php. | | npm run build | Library build into dist/ (ES + UMD + single CSS). | | npm run preview | Serve the built dist/ output. |

No test suite, linter, or typecheck is configured.