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

@blitheforge/media-library

v1.1.1

Published

Self-contained React media library with pre-built CSS. Works with or without Tailwind in your app.

Readme

@blitheforge/media-library

Production-ready React media library with pre-built, scoped CSS — no Tailwind required in your app. Works in Next.js, Vite, plain React, or vanilla JavaScript (headless API).

Features

  • Self-contained CSS — import one file; styles are scoped to .bfml-root and won't break your app layout
  • Nested folder management — browse, create, and delete folders
  • File upload — click to upload or drag-and-drop; files upload one-by-one with live preview cards in the grid
  • 5 MB client-side limit — oversized files show a warning toast and are skipped (configurable via MAX_MEDIA_UPLOAD_BYTES)
  • Search — filter files in the current folder
  • RBAC / capabilities — upload, create-folder, and delete UI is driven by your API capabilities response
  • Delete confirmation — custom confirm dialog (not native confirm())
  • Toast notifications — success, error, and warning toasts (top-right inside the panel/modal)
  • Responsive — mobile folder drawer, full-screen modal on small screens
  • Theme sync — inherits host light/dark mode via CSS variables (theme="sync")
  • Three display modes — form pickers (MediaPicker), modal (MediaLibraryModal), embedded widget (MediaLibraryWidget)
  • Type-safe headless clientcreateMediaLibraryClient for custom integrations

Install

npm install @blitheforge/media-library

Peer dependencies:

npm install react react-dom

Setup

1. Import the bundled CSS once

The package ships a complete CSS file built from Tailwind at publish time. Your app does not need Tailwind.

// React — app/layout.tsx, main.tsx, etc.
import "@blitheforge/media-library/styles.css";
<!-- Vanilla HTML / any framework -->
<link rel="stylesheet" href="/node_modules/@blitheforge/media-library/dist/style.css" />

Styles are scoped under .bfml-root, so they will not override your app's hidden, flex, lg:block, etc.

If your app also uses Tailwind, import the library CSS before or after your globals — both work with scoped styles. You do not need @source for this package.

2. React (Next.js App Router)

Add the package to transpilePackages in next.config.ts:

const nextConfig = {
  transpilePackages: ["@blitheforge/media-library"]
};
export default nextConfig;

3. Vanilla JavaScript (no React)

Use the headless client for upload/list/delete from any JS project:

<link rel="stylesheet" href="https://unpkg.com/@blitheforge/media-library/dist/style.css" />
<script type="module">
  import { createMediaLibraryClient } from "https://unpkg.com/@blitheforge/media-library/dist/client.js";

  const client = createMediaLibraryClient({
    listUrl: "/api/media",
    uploadUrl: "/api/media/upload",
    createFolderUrl: "/api/media/folders",
    deleteUrl: "/api/media"
  });

  const listing = await client.list("");
  console.log(listing.files);

  // Upload from a file input
  document.querySelector("#files").addEventListener("change", async (event) => {
    const files = [...event.target.files];
    const uploaded = await client.upload("", files);
    console.log(uploaded);
  });
</script>

Node / bundler:

import { createMediaLibraryClient } from "@blitheforge/media-library/client";

React UI components (MediaPicker, MediaLibraryModal, etc.) still require React. The /client export has no React dependency.

4. Monorepo / workspace

Link the local package via pnpm workspace:

# pnpm-workspace.yaml
packages:
  - "package"
  - "."
// package.json
{
  "dependencies": {
    "@blitheforge/media-library": "workspace:*"
  }
}

Rebuild after package changes:

cd package && pnpm build

Publishing

Pushes to main run .github/workflows/publish.yml. The workflow typechecks and builds the package, then publishes it only when the version in package.json is not already present on npm.

Before the first automated publish:

  1. Create an npm granular access token with Bypass 2FA enabled and Read and write package permission for the @blitheforge scope.
  2. In the GitHub repository, open Settings > Secrets and variables > Actions.
  3. Add the token as a repository secret named NPM_TOKEN.

For each release, bump the package version and push to main:

npm version patch
git push --follow-tags

Theming

Default is theme="sync" — the library reads your app's CSS variables:

| Host variable | Used for | |---------------|----------| | --background | Page background | | --foreground | Text | | --surface | Panel/card background | | --border | Borders | | --primary | Buttons, active states | | --destructive | Delete actions | | --accent | Hover/secondary surfaces | | --warning | Warning toasts |

When your site toggles dark mode (e.g. [data-theme="dark"] on <html>), the media library follows automatically.

Standalone (no shared design tokens):

<MediaPicker name="image" theme="light" />
<MediaPicker name="image" theme="dark" />

Or via config:

<MediaPicker name="image" config={{ theme: "sync" }} />

Theme modes: "sync" | "light" | "dark"


Components

| Export | Use case | |--------|----------| | MediaPicker | Single file field for forms — opens modal, shows live preview | | MediaPickerMulti | Multiple attachments with preview cards | | MediaLibraryModal | Full-screen/modal picker with Done button | | MediaLibraryWidget | Embedded admin panel — fixed width/height, no overlay | | MediaLibraryPanel | Low-level panel (variant="modal" or "embedded") | | MediaPreview | Standalone image/PDF preview block | | createMediaLibraryClient | Headless API client |


MediaPicker — single file (forms)

Best for donation proofs, expense vouchers, profile photos, etc.

import { MediaPicker } from "@blitheforge/media-library";

export function DonationForm() {
  return (
    <form>
      <MediaPicker
        name="proofUrl"
        label="Donation proof"
        accept={["image", "pdf"]}
        onChange={(path) => console.log(path)}
      />
    </form>
  );
}

Props: name, label, value, defaultValue, onChange, accept, config, theme, title, description, className


MediaPickerMulti — multiple attachments

import { MediaPickerMulti } from "@blitheforge/media-library";

<MediaPickerMulti
  name="attachments"
  label="Attachments"
  max={5}
  accept={["image", "pdf"]}
  onChange={(paths) => console.log(paths)}
/>

MediaLibraryModal — picker modal

Use when you need a custom trigger but still want the picker flow (select file → Done).

import { useState } from "react";
import { MediaLibraryModal } from "@blitheforge/media-library";

function CustomTrigger() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>Browse media</button>
      <MediaLibraryModal
        open={open}
        onClose={() => setOpen(false)}
        onSelect={(file) => {
          console.log(file.url);
          setOpen(false);
        }}
        accept={["image", "pdf"]}
      />
    </>
  );
}

Modal behavior:

  • Portal overlay with backdrop blur
  • Escape key closes (unless delete confirm is open)
  • Mobile: full viewport height; desktop: centered dialog
  • Footer with Selected label and Done button

MediaLibraryWidget — embedded admin panel

Use for a dedicated Media Library admin page — no modal, no overlay. Folders and files are always visible.

import { MediaLibraryWidget } from "@blitheforge/media-library";

export function MediaAdminPage() {
  return (
    <MediaLibraryWidget
      width="100%"
      height="calc(100vh - 200px)"
      title="Media Library"
      description="Create folders, upload files, and manage media."
      accept={["image", "pdf"]}
    />
  );
}

Width & height props

| Prop | Type | Default | Examples | |------|------|---------|----------| | width | string \| number | "100%" | 800, "70vw", "100%" | | height | string \| number | 640 | 720, "70vh", "calc(100vh - 200px)" |

  • Numbers → pixels (720720px)
  • Strings → passed as CSS values ("100%", "70vh")
  • calc() — use proper CSS syntax with spaces: "calc(100vh - 200px)"
  • Shorthand"100vh-200px" is auto-converted to calc(100vh - 200px)

Widget vs modal

| | MediaLibraryWidget | MediaLibraryModal | |--|----------------------|---------------------| | Display | Inline on page | Overlay / portal | | Close button | No | Yes | | Done footer | Hidden by default | Always shown | | Loads | On mount | When open={true} | | Best for | Admin manage page | Form file picking |

Selectable widget (picker in embedded mode)

<MediaLibraryWidget
  width={900}
  height={600}
  selectable
  onSelect={(file) => console.log(file.path)}
/>

MediaLibraryPanel — low-level

Build custom layouts by composing the panel directly.

import { MediaLibraryPanel } from "@blitheforge/media-library";

<MediaLibraryPanel
  active
  variant="embedded"
  selectable={false}
  title="Files"
  accept={["image"]}
  className="h-full"
/>

Props: active, variant ("modal" | "embedded"), selectable, onClose, onSelect, config, theme, title, description, accept, className


MediaPreview

Standalone preview for a stored path/URL:

import { MediaPreview } from "@blitheforge/media-library";

<MediaPreview path="/uploads/media/photo.png" alt="Donation proof" />

Configure API URLs

All components accept an optional config prop:

<MediaPicker
  name="imageUrl"
  config={{
    listUrl: "/api/media",
    uploadUrl: "/api/media/upload",
    createFolderUrl: "/api/media/folders",
    updateUrl: "/api/media",
    deleteUrl: "/api/media",
    rootLabel: "Root",
    theme: "sync"
  }}
/>

Default URLs (relative to your app):

| Key | Default | |-----|---------| | listUrl | /api/media | | uploadUrl | /api/media/upload | | createFolderUrl | /api/media/folders | | updateUrl | /api/media | | deleteUrl | /api/media | | rootLabel | "Root" |


Backend API contract

All responses must follow:

{ "success": true, "data": ... }

Errors:

{
  "success": false,
  "error": { "code": "VALIDATION_ERROR", "message": "Human readable message" }
}

List — GET {listUrl}?path=&q=

Query params:

  • path — current folder path (empty = root)
  • q — search query (optional)

Response:

{
  "success": true,
  "data": {
    "path": "donations",
    "folders": [
      { "name": "2026", "path": "donations/2026" }
    ],
    "files": [
      {
        "name": "receipt.png",
        "path": "donations/receipt.png",
        "url": "/uploads/media/donations/receipt.png",
        "size": 12345,
        "mimeType": "image/png",
        "updatedAt": "2026-06-06T00:00:00.000Z"
      }
    ],
    "capabilities": {
      "view": true,
      "upload": true,
      "createFolder": true,
      "delete": true,
      "rename": true,
      "select": true
    }
  }
}

Capabilities (RBAC)

The UI reads capabilities from every list response to show or hide actions:

| Capability | Controls | |------------|----------| | view | Access to browse (required) | | upload | Upload button + drag-and-drop | | createFolder | Folder create form in sidebar | | delete | Delete buttons on files/folders | | rename | Reserved for future rename UI | | select | File selection (picker flows) |

If capabilities is omitted, all actions default to enabled.

Upload — POST {uploadUrl}

multipart/form-data:

  • path — target folder path (empty string = root)
  • files — one or more files

The UI uploads one file at a time sequentially and shows a preview card per file in the grid.

Recommended server limit: 5 MB per file (matches client-side check).

Create folder — POST {createFolderUrl}

{
  "path": "donations",
  "name": "2026",
  "nested": true
}
  • nested: true — create inside path
  • nested: false — create at root level

Rename — PATCH {updateUrl}

{
  "path": "donations/old.png",
  "newName": "new.png",
  "type": "file"
}

type: "file" | "folder"

Delete — DELETE {deleteUrl}

{
  "path": "donations/old.png",
  "type": "file"
}

Deleting a folder should remove all nested content on the server.


Headless client

Use without UI for scripts, tests, or custom interfaces:

import { createMediaLibraryClient } from "@blitheforge/media-library";

const client = createMediaLibraryClient({
  listUrl: "/api/media",
  uploadUrl: "/api/media/upload",
  createFolderUrl: "/api/media/folders",
  updateUrl: "/api/media",
  deleteUrl: "/api/media"
});

const listing = await client.list("donations", "receipt");
await client.createFolder("", "archive", true);
await client.uploadOne("donations", file);
await client.rename("donations/old.png", "new.png", "file");
await client.remove("donations/old.png", "file");

Utility exports

import {
  MAX_MEDIA_UPLOAD_BYTES,       // 5 * 1024 * 1024
  isFileWithinUploadSizeLimit,
  formatUploadSizeLimit,        // "5 MB"
  fileMatchesAccept,
  fileMatchesAcceptForUpload,
  fileNameFromPath,
  isImagePath,
  bfmlRootProps,
  resolveThemeMode
} from "@blitheforge/media-library";

Upload behavior

  1. User selects files (click or drag-and-drop)
  2. Client validates type (accept prop) and size (5 MB default)
  3. Oversized files → warning toast, skipped
  4. Invalid type → error toast, skipped
  5. Valid files → preview cards appear in the file grid
  6. Each file uploads sequentially; card removed on success
  7. Folder list refreshes silently after each successful upload
  8. Success toast when batch completes

File type filtering

Pass accept to restrict allowed types:

accept={["image"]}           // images only
accept={["pdf"]}             // PDF only
accept={["image", "pdf"]}    // both (default for admin widget)

Omit accept to allow all types returned by the API.


TypeScript types

import type {
  MediaFile,
  MediaFolder,
  MediaListing,
  MediaCapabilities,
  MediaLibraryConfig,
  MediaLibraryModalProps,
  MediaLibraryWidgetProps,
  MediaLibraryPanelProps,
  MediaPickerProps,
  MediaPickerMultiProps,
  MediaLibraryThemeMode
} from "@blitheforge/media-library";