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

@basementstudio/sanity-media-library

v0.2.0

Published

Folder-based media library tool and image asset source for Sanity Studio, with optional per-workspace scoping.

Downloads

238

Readme

@basementstudio/sanity-media-library

A folder-based media library for Sanity Studio: a Media tool for browsing, organizing and cleaning up image assets, plus a matching image asset source so editors pick from the same library on every image field.

It works on a plain Studio out of the box, and can additionally scope assets per workspace — several sites sharing one dataset, each seeing only its own images.

Features

  • File-explorer UI: nested folders, grid/list views, sorting, search
  • Folders nest to any depth, with a breadcrumb trail to navigate back up
  • Drag and drop assets into folders (single or multi-select), including onto breadcrumb ancestors to move them up
  • Upload straight into the current folder
  • Asset dialog: rename, alt text, folder, dimensions/size/MIME info
  • "Used by" tab listing the published documents referencing an asset
  • Bulk delete and one-click cleanup of unreferenced assets — anything still in use is detected and kept
  • Optional per-workspace scoping, with a control to reassign an asset to other scopes
  • Image asset source that browses the same library from any image field, with the same grid/list views as the Media tool

Install

npm install @basementstudio/sanity-media-library
# or
bun add @basementstudio/sanity-media-library

Peer dependencies: sanity v5, @sanity/ui v3, @sanity/icons v3, react and react-dom v19.

Usage

// sanity.config.ts
import { defineConfig } from "sanity"
import { structureTool } from "sanity/structure"
import { mediaLibrary } from "@basementstudio/sanity-media-library"

export default defineConfig({
  name: "default",
  projectId: "…",
  dataset: "production",
  plugins: [structureTool(), mediaLibrary()],
})

That registers a Media tool at /studio/media, a mediaFolder document type, and an image asset source that replaces the built-in ones.

Scoped to a workspace

Scoping partitions one dataset between several Studio workspaces. Each workspace passes its own scope.value; the tool and the asset source then only ever show assets stamped with it.

const SITES = [
  { name: "business", title: "Business" },
  { name: "money", title: "Money" },
]

export default defineConfig(
  SITES.map((site) => ({
    name: site.name,
    title: site.title,
    basePath: `/studio/${site.name}`,
    projectId: "…",
    dataset: "production",
    plugins: [
      structureTool(),
      mediaLibrary({
        scope: {
          value: site.name,
          // Assets predating scoping carry no `sites` field — treat them as
          // belonging to this scope.
          fallback: "business",
          // Shown in the asset dialog so an editor can share an asset.
          options: SITES,
        },
      }),
    ],
  }))
)

An asset can belong to several scopes at once: uploads stamp sites: [scope], and the asset dialog toggles the rest. Identical files dedupe to one asset document in Sanity, so the scope is merged rather than overwritten.

Configuration

| Option | Default | Description | | ------------- | ----------- | ------------------------------------------------------------------------------------------------------- | | scope | – | Enables per-workspace scoping (see below). Omit for a single shared library. | | folderType | mediaFolder | Document type used for folders. | | toolName | media | Tool name, and the Studio URL segment. | | toolTitle | Media | Tool title in the navbar. | | assetSource | replace | replace hides the built-in image sources, append keeps them, false registers none. | | apiVersion | 2025-02-19 | API version for the Studio client. |

scope

| Option | Default | Description | | ------------- | ------- | ---------------------------------------------------------------------------- | | value | – | Required. The scope this workspace browses and uploads into. | | fallback | – | Scope that assets with no scope field belong to. Set it on the legacy scope. | | options | [] | All scopes, listed in the asset dialog so an asset can be reassigned. | | assetField | sites | Array field stamped on sanity.imageAsset. | | folderField | site | String field stamped on folder documents. |

Data model

  • Folders are documents (mediaFolder by default) whose title holds the folder's full pathbrand/logos. An asset points at one through the same path string in its folder field. Folders are documents rather than a derived list so an empty folder can be created and persists.
  • Nesting is a materialized path, not a parent reference. That keeps listing and filtering to plain string comparisons — GROQ cannot express recursive traversal — and means a flat folder created before nesting is already a valid one-segment path, so there is nothing to migrate. The cost is that renaming or moving a folder has to re-point its descendants, which the plugin does in a single transaction.
  • Intermediate folders can be implied. An asset in brand/logos puts brand on screen even when no folder document exists for it, so a path is never unreachable. Creating brand/logos does not create a brand document.
  • Folder names cannot contain /; nesting comes from creating a folder while inside another one.
  • Assets are untouched sanity.imageAsset documents. Scoping adds an array field (sites) and organizing adds folder; nothing else is duplicated, so the assets stay usable by any query or plugin.
  • Deletion is reference-safe. Before deleting, the plugin counts incoming references per asset and only removes the unreferenced ones, reporting how many were kept. Drafts and release versions count as references. Deleting a folder cascades through its nested folders, and the confirmation says how many folders and images that adds up to. Deletion is a hard delete — Sanity removes the underlying file, so there is no undo beyond your project's history retention and backups.

Notes

  • Native (drag-onto-the-field) uploads bypass the plugin and are therefore unscoped and unfoldered. Assign them from the Media tool, or set assetSource: "replace" (the default) so browsing always goes through the library.
  • Search is recursive: inside a folder it matches assets in that folder and everything nested below it, since a search that stopped at the current level would miss most of a nested library. Browsing without a search term stays scoped to the folder's own contents.
  • Folder documents appear in the default document type list of structureTool. Filter them out in a custom structure if you don't want editors there — the plugin already removes the type from the global "create new" menu.
  • Drafts and releases protect an asset. Deletion checks and the "Used by" tab count every document that references an asset — published, drafts and release versions alike — so an image used only in an unpublished draft is never deleted by cleanup. These reads run under the raw perspective for exactly that reason; a published perspective reports a draft-only asset as unreferenced. The tab marks such documents Unpublished, so it is clear why an asset was kept.

Development

bun install
bun run check   # typecheck
bun run build   # emit dist/
bun test

Releases run on changesets: add one with bun run changeset in every PR that changes src/. Merging to main opens (or updates) a release PR; merging that publishes to npm.