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

frameset

v0.10.0

Published

A lightweight viewer for iterating on HTML frames alongside a real codebase. A frame is an HTML fragment that imports real components and styles. A `frameset.json` manifest names those files, and Frameset wraps them into full documents at serve time and s

Readme

Frameset

A lightweight viewer for iterating on HTML frames alongside a real codebase. A frame is an HTML fragment that imports real components and styles. A frameset.json manifest names those files, and Frameset wraps them into full documents at serve time and shows them in a live viewer with hot reload.

Storybook targets isolated component stories. Figma targets visual design. Frameset targets the space between: composing real components into real screens quickly, especially in agent-driven workflows where editing plain HTML is the simplest thing that can work.

Quick start

From your project root:

frameset

This reads ./frameset.json, starts a dev server on http://127.0.0.1:4400/, opens the viewer in a browser, and watches for changes. The manifest is the whole setup — it names which files are frames, what they are called, and how they are grouped in the sidebar.

Run frameset from the directory holding frameset.json. That file names the root to serve, which Vite watches and every path in the manifest is relative to, so frames can import sibling code (e.g. ../components/). A missing frameset.json is a startup error.

To work on the viewer UI itself, run:

npm run dev

This runs Frameset against the local example/ project, which is the default sandbox for iterating on the viewer UI and example frames together.

Project setup

A Frameset project is a frameset.json naming *.html frame files that live next to the code they use:

my-project/
  frameset.json
  frames/
    dashboard.html
    login.html
  components/
    auth/
      sign-in.html
      sign-in.js
    nav-bar.js
    metric-card.js
{
  "root": ".",
  "sidebar": [
    { "type": "frame", "path": "frames/dashboard.html", "title": "Dashboard" },
    { "type": "folder", "title": "Onboarding", "children": [
        { "type": "frame", "path": "components/auth/sign-in.html", "title": "Sign in" },
        { "type": "frame", "path": "frames/login.html" }
    ]}
  ]
}

root is the directory Frameset serves, relative to frameset.json"." is the manifest's own directory. Every path in the file is relative to that root, and so is every frame id.

sidebar is the sidebar, top to bottom: order in the file is order on screen. There are two kinds of entry, told apart by type:

| Entry | Means | |---|---| | { "type": "frame", "path": "path.html", "title": "..." } | one frame. Untitled, it takes its filename. | | { "type": "folder", "title": "...", "children": [ ... ] } | a collapsible group, nested to any depth. title is required. | | { "type": "section", "title": "...", "children": [ ... ] } | the same, drawn as a muted caret row rather than a folder — for grouping without another level of folder. |

Every frame is listed by hand — nothing is discovered by scanning directories, so the sidebar holds exactly what you put in it, named and ordered how you want. A folder is a sidebar grouping, not a directory: frames anywhere under the root can sit in one together.

Every path is relative to root and must stay under it. A frame it names that doesn't exist yet shows as a broken sidebar entry until you create the file.

Frames

A frame is a body-only HTML fragment — no <!DOCTYPE>, <html>, <head>, or <body> tags. Frameset wraps it into a full document automatically.

<!-- frames/login.html -->
<script type="module" src="../components/login-panel.js"></script>

<style>
  body { background: #f8fafc; }
  :root[data-theme="dark"] body { background: #020617; }
</style>

<div class="page">
  <login-panel title="Welcome back" primary-label="Continue"></login-panel>
</div>
  • The sidebar name comes from the manifest's title, or the filename when it gives none. A frame's own <title> element is not read.
  • <script> and <style> tags work normally. Relative paths resolve from the frame file's location.
  • Frames are served through Vite, so any transforms from your vite.config.js apply automatically.

<frameset-frame>

Every frame gets one custom element for free, with nothing to import — it shows another frame at a size you choose, named above its top-left corner the way a Figma frame is labelled, with the embedded frame's declared controls in a strip below:

<frameset-frame src="/frames/login" label="Login" width="420" height="600"></frameset-frame>

src is required and takes a path to a frame — relative to the frame you're writing in, or absolute from the project root — and the target needn't be listed in the manifest. Use it for sheets: a frame whose subject is other frames (an overview of screens, one design at two breakpoints). It is transparent until you give it a background, and border adds a 1px grey hairline; beyond the size, the element styles nothing. See specs/frameset-frame.md.

A frame is rendered as a plain webpage: its body fills the iframe edge-to-edge under a minimal margin reset. A frame paints no background unless the design calls for one — unpainted, the viewer's own ground shows behind it. See skill/SKILL.md for the authoring conventions.

Commands

frameset — start dev server

frameset [options]

No arguments: the manifest at ./frameset.json says what to serve.

| Option | Description | Default | |---|---|---| | --port <number> | Dev server port | 4400 | | --host <host> | Network interface to bind (0.0.0.0 for all) | 127.0.0.1 | | --allowed-hosts <hosts> | Comma-separated hostnames to accept, or all | — |

Viewer

The viewer shows the manifest in a sidebar — frames, and folders you can collapse — and an iframe preview of the selected frame. If the frame declares controls, a toolbar with dropdown controls appears below the preview once it loads. Collapsed folders are remembered between sessions; the folders around the selected frame are always opened on load.

A frame stays loaded once you visit it — switching frames shows a different one rather than reloading it, so scroll position, an open menu or half-typed input survive navigating away and back. Editing a file still reloads everything.

The selected frame is stored in the URL hash, so you can bookmark or share links like http://127.0.0.1:4400/#frames/dashboard.

Direct frame routes

Each frame is also available at its own URL — the chromeless, directly-linkable frame document, with no viewer around it:

http://127.0.0.1:4400/frame/frames/dashboard
http://127.0.0.1:4400/frame/components/auth/sign-in

A frame's id is its path under the root without .html, so the route carries the slashes through.

Every .html file under the root is served here, whether or not the manifest lists it. The manifest decides the sidebar, not what can be rendered — so a fragment is linkable the moment you write it, and one meant only to be pulled into another page needs no sidebar row at all.

These are full HTML pages, useful for testing with Playwright, visual diffing, or accessibility checks — no special story format needed. Since the controls bar lives in the viewer, controls sit at their declared defaults here.

Vite integration

Frameset creates a Vite dev server rooted at your project. If you have a vite.config.js, Frameset inherits your plugins, aliases, PostCSS config, and framework transforms. Frameset only overrides what it needs to control (server port, host, and its own routes).

Viewer-only dependencies belong to Frameset itself. If the viewer imports a linked local package, Frameset needs an explicit Vite alias for it in src/server.ts so the viewer still resolves correctly when the dev server root points at another project.

Testing

Since frames are plain URLs, standard browser testing tools work directly:

// Playwright example
await page.goto("http://127.0.0.1:4400/frame/frames/dashboard");
await expect(page.locator("metric-card")).toBeVisible();