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

@lnsy/file-clerk

v0.1.0

Published

A tiny, browser-only set of Custom Elements for uploading, storing, listing, viewing, splitting, and archiving files — all client-side with IndexedDB (via localforage). No build step required.

Readme

File Clerk

A tiny, browser-only set of Custom Elements for uploading, storing, listing, viewing, splitting, and archiving files — all client-side with IndexedDB (via localforage). No build step required.

This repo includes the following web components:

  • <file-uploader> — file picker UI that emits a fileuploaded event with a Data URL
  • <file-clerk> — persistence and simple UI (optional) backed by localforage
  • <file-viewer> — renders a Data URL by MIME type (images, video, audio, PDF, text)
  • <file-splitter> — splits and rejoins Data URL strings (14 KB chunks)
  • <file-archive> — export/import all saved files as a ZIP (client-only)

Quick links

  • Hosted ESM bundle (recommended): https://lindseymysse.com/file-clerk/file-clerk.min.js
  • Example page in this repo: index.html

Quick start (copy/paste)

Include the hosted ESM bundle and drop the elements into your page. This registers all custom elements globally.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>File Clerk — Quick Start</title>
    <!-- Registers: file-uploader, file-clerk, file-viewer, file-splitter, file-archive -->
    <script type="module" src="https://lindseymysse.com/file-clerk/file-clerk.min.js"></script>
  </head>
  <body>
    <file-uploader></file-uploader>
    <file-clerk id="file_clerk" verbose></file-clerk>
    <file-viewer id="file_viewer"></file-viewer>
    <file-splitter id="file_splitter"></file-splitter>
    <file-archive id="file_archive" target="#file_clerk" verbose></file-archive>

    <script type="module">
      // Wait until file-clerk is defined before calling methods early on
      const fileClerk = document.getElementById('file_clerk');
      const fileViewer = document.getElementById('file_viewer');
      const fileSplitter = document.getElementById('file_splitter');
      const fileUploader = document.querySelector('file-uploader');

      const ready = customElements.whenDefined('file-clerk').then(() => {
        if (customElements.upgrade) customElements.upgrade(fileClerk);
      });

      // Uploader -> Clerk (save)
      fileUploader.addEventListener('fileuploaded', async (e) => {
        await ready;
        const { fileData, name, notes } = e.detail;
        fileClerk.saveFile(name, fileData, notes);
      });

      // Clerk (open) -> Splitter -> Viewer
      fileClerk.addEventListener('file-opened', (e) => {
        const chunks = fileSplitter.splitFiles(e.detail.contents);
        const rejoined = fileSplitter.joinFiles(chunks);
        fileViewer.openFile(rejoined);
      });
    </script>
  </body>
</html>

Run it locally with any static server:

  • macOS/Linux: python3 -m http.server 8000
  • Then open http://localhost:8000/index.html

Components, APIs, and events

Below are the public surfaces you’ll typically use. All persistence happens client-side with IndexedDB via localforage.

<file-uploader>

  • Purpose: lets the user pick a file; previews name/size; emits fileuploaded.
  • Event: fileuploaded with detail: { fileData, name, notes }
    • fileData is a Data URL (data:;base64,...)

Example: listen for fileuploaded

const uploader = document.querySelector('file-uploader');
uploader.addEventListener('fileuploaded', (e) => {
  const { fileData, name, notes } = e.detail;
  console.log('Got file:', { name, notes, bytes: fileData.length });
});

<file-clerk>

  • Purpose: persistence and simple UI when verbose is set.
  • Backing store: localforage (IndexedDB with localStorage fallback)
  • Methods:
    • await saveFile(filename, contents, metadata)
    • await deleteFile(id)
    • await listFiles() => [{ id, filename, contents, metadata }]
    • await openFile(id) => dispatches file-opened with the file data
  • Events:
    • file-opened with detail: { filename, contents, metadata }

Example: programmatic save/list/open

const clerk = document.querySelector('file-clerk');
await customElements.whenDefined('file-clerk');

// Save a text file programmatically
const contents = 'data:text/plain;base64,' + btoa('Hello, File Clerk!');
await clerk.saveFile('hello.txt', contents, { author: 'you' });

// List all files
const files = await clerk.listFiles();
console.table(files.map(f => ({ id: f.id, name: f.filename })));

// Open the first file (fires `file-opened`)
if (files[0]) await clerk.openFile(files[0].id);

Example: build your own file list UI (no verbose)

<file-clerk id="clerk"></file-clerk>
<ul id="list"></ul>
<script type="module">
  const clerk = document.getElementById('clerk');
  const list = document.getElementById('list');
  await customElements.whenDefined('file-clerk');

  async function refresh() {
    list.innerHTML = '';
    for (const f of await clerk.listFiles()) {
      const li = document.createElement('li');
      li.textContent = f.filename;
      const open = document.createElement('button');
      open.textContent = 'Open';
      open.onclick = () => clerk.openFile(f.id);
      const del = document.createElement('button');
      del.textContent = 'Delete';
      del.onclick = async () => { await clerk.deleteFile(f.id); refresh(); };
      li.append(open, del);
      list.append(li);
    }
  }
  refresh();
</script>

  • Purpose: render a file Data URL by MIME type
  • Supports: image/jpeg, image/png, video/mp4, audio/mpeg, application/pdf, text/plain
  • Method: openFile(dataUrl)

Example: open a text file

const viewer = document.querySelector('file-viewer');
const dataUrl = 'data:text/plain;base64,' + btoa('README example');
viewer.openFile(dataUrl);

  • Purpose: split/join long Data URL strings (14 KB chunks)
  • Methods:
    • splitFiles(dataUrl) => [{ index, data }] chunks
    • joinFiles(chunks) => dataUrl

Example: round-trip

const splitter = document.querySelector('file-splitter');
const chunks = splitter.splitFiles(myDataUrl);
const rejoined = splitter.joinFiles(chunks);
console.log(rejoined === myDataUrl); // true

  • Purpose: export/import your File Clerk storage as a single ZIP (client-only)
  • Attributes:
    • target: CSS selector to locate the instance (default: #file_clerk)
    • verbose: render buttons for Export ZIP / Import ZIP
  • Methods (programmatic):
    • await exportArchive() — triggers download of a ZIP containing files/ and manifest.json
    • await importArchive(fileOrBlob) — imports from a ZIP generated by exportArchive()

Example: programmatic export/import

const archive = document.querySelector('file-archive');

// Export all entries to a ZIP
await archive.exportArchive();

// Import from a chosen .zip file (via <input type="file" />)
const input = document.createElement('input');
input.type = 'file';
input.accept = '.zip';
input.onchange = async () => {
  const file = input.files && input.files[0];
  if (file) await archive.importArchive(file);
};
input.click();

End-to-end wiring (from this repo’s index.html)

Below is the exact wiring used in index.html — uploader -> clerk.saveFile, and on open: split -> view.

    // Ensure custom element is defined/upgraded before calling its methods
    const onClerkReady = customElements.whenDefined('file-clerk').then(() => {
      if (customElements.upgrade) {
        customElements.upgrade(fileClerkEl);
      }
    });

    // Wire uploader -> clerk save
    fileUploader.addEventListener('fileuploaded', async (event) => {
      await onClerkReady;
      const { fileData, name, notes } = event.detail;
      fileClerkEl.saveFile(name, fileData, notes);
    });

    // Initial list
    onClerkReady.then(() => {
      if (typeof fileClerkEl.listFiles === 'function') {
        fileClerkEl.listFiles().then((res) => console.log(res));
      }
    });

    // Open -> split -> view
    fileClerkEl.addEventListener('file-opened', async (e) => {
      const rejoined_file = fileSplitterEl.joinFiles(
        fileSplitterEl.splitFiles(e.detail.contents)
      );
      fileViewerEl.openFile(rejoined_file);
    });

Local development

There is no build step. Serve the directory with a static server to avoid browser restrictions when fetching Data URLs for text rendering.

  • Start a server in this directory:
    • python3 -m http.server 8000
  • Open the app:
    • open http://localhost:8000/index.html

Notes

  • The minified bundle registers all elements. If you want to use the source modules directly, you’ll need a bundler or import maps for external deps (client-zip, unzipit). The hosted bundle is the simplest path.

Data model and storage

file-clerk stores entries as:

{
  "id": "<uuid>",
  "filename": "<string>",
  "contents": "data:<mime>;base64,<...>",
  "metadata": { "notes": "<string>", "...": "any" }
}
  • Storage is powered by localforage, using IndexedDB when available.
  • Clearing storage during development: either
    • use methods to list and delete each entry, or
    • clear site data for http://localhost:8000 in your browser’s devtools.

Example: clear via API

const clerk = document.querySelector('file-clerk');
await customElements.whenDefined('file-clerk');
for (const f of await clerk.listFiles()) {
  await clerk.deleteFile(f.id);
}

Tips, limits, and troubleshooting

  • Use a local HTTP server. Opening index.html with file:// can sometimes work, but http://localhost ensures consistent behavior.
  • File sizes: Data URLs are base64-encoded and can get large quickly. Browsers impose IndexedDB quotas; keep files reasonably small (a few MB) for best results.
  • Wait for custom elements to be defined before calling methods: await customElements.whenDefined('file-clerk').
  • Text rendering uses fetch(dataUrl). Serving over http:// avoids some browser edge cases.
  • MIME types: If a Data URL has an unknown or octet-stream MIME, tries to correct it during import using the manifest or filename extension.

Extending

Because these are standard Custom Elements, you can:

  • Compose them with your own UI — hide verbose UIs and build your own list/details views.
  • Listen to their events (fileuploaded, file-opened) and add your own application logic.
  • Store extra metadata alongside files; it’s just an object argument to saveFile().

License

MIT (or the license of this repository, if specified elsewhere).