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

schublade

v0.1.0

Published

Storybook-like component workshop served from a Rust CLI. No JS toolchain in your catalog repo.

Downloads

167

Readme

Schublade

A Storybook-like component workshop that runs from a Rust CLI. Consumers start a local server; they do not add a JavaScript toolchain to their repository.

This is a POC/MVP. Feature parity with Storybook is not a goal. There is no plugin system.

The bundled demo catalog is a small component kit: left story nav with grouped sub-drawers, an isolated center canvas, right-hand controls, and a Code Usage / a11y inspector under the preview.

Install

The CLI is a Rust binary published to npm the same way Biome and git-cliff do: a root schublade package plus optional platform packages (@schublade/cli-darwin-arm64, @schublade/cli-linux-x64, …). npm installs only the package that matches your OS and CPU. The schublade bin is a small Node wrapper that require.resolves that binary and execs it. There is no postinstall and no download from GitHub Releases.

Consumers need npm (or another Node 18+ package runner). They do not add a JS toolchain, bundler, or Rust to the catalog repo.

npx schublade serve
npx schublade build
npm install -g schublade
schublade serve --config ./schublade.toml

Supported prebuilt targets: macOS (arm64, x64), Linux glibc (x64, arm64), Windows (x64, arm64). Alpine/musl is not supported.

From this repository, npx schublade builds with Cargo when the matching @schublade/cli-* package is not installed. SCHUBLADE_BINARY=/path/to/schublade overrides the resolved binary.

Run

npx schublade serve
npx schublade serve --port 47291
npx schublade serve --config ./schublade.toml
npx schublade serve --config ./schublade.toml --catalog ./catalog.toml
npx schublade serve --config ./schublade.toml --stories ./components

The workshop listens on http://127.0.0.1:47291 by default (0.0.0.0:47291 so it is reachable in this environment). schublade with no subcommand also starts the server.

Agents can read the catalog as markdown from the same server: GET /AGENTS.md (index) and GET /{id}/AGENTS.md (for example /button-ghost/AGENTS.md). Those pages use the story catalog — the same usage snippet and controls as the workshop — not a second story format and not component source.

serve watches the resolved schublade.toml, catalog.toml, and stories/component tree. Catalog and story discovery reload in place — the HTTP server stays up. The workshop UI picks up the new catalog over /api/events (or /api/generation if EventSource is missing) and re-renders the iframe. Changing bind host/port in the config does not rebind; restart for that.

When hacking on the CLI itself:

cargo run -- serve
cargo run -- serve --config ./schublade.toml

Static build

Write a self-contained HTML workshop that runs without the Rust server. Controls, theme, and a11y keep working — rendering happens in the browser.

npx schublade build
npx schublade build --out dist
npx schublade build --config examples/story-files/schublade.toml --out dist/story-files

The folder contains index.html (catalog baked in), preview.html, CSS/JS (including React for JSX stories), bootstrap.json, a small vercel.json, plus AGENTS.md and {id}/AGENTS.md — the same files the live server serves. Hash routes (#/button) do not need SPA rewrites.

Deploy that folder to any static host — Vercel, Netlify, nginx, GitHub Pages, or python -m http.server. Create one Vercel project per example yourself and point the project at that example’s output folder. Schublade does not talk to Vercel.

bash examples/build-all.sh
# writes dist/examples/<name>/

Story files

React prop documentation is extracted from the imported component’s JavaScript/JSX AST. Leading comments on destructured props become descriptions, literal parameter defaults provide defaults and types, and CSF args / argTypes fill metadata gaps while continuing to define the interactive story controls.

Define the actual component in HTML or React. The story file imports that component and declares args / argTypes for the workshop state. There is no second copy of the markup and no code = "<Button…>" usage template.

The CLI discovers *.stories.js / *.stories.jsx first. *.stories.toml remains a fallback.

// components/button.jsx
export function Button({
  /** Visible text inside the button. */
  label,
  /** Visual treatment of the button. */
  variant = 'primary',
  /** Prevents interaction. */
  disabled = false,
}) {
  return (
    <button className="btn" data-variant={variant} disabled={disabled}>
      {label}
    </button>
  );
}
// components/button.stories.jsx
import { Button } from './button.jsx';

export default {
  title: 'Components/Button',
  component: Button,
  args: {
    label: 'Save changes',
    variant: 'primary',
    disabled: false,
  },
  argTypes: {
    variant: { control: 'select', options: ['primary', 'ghost'] },
    disabled: { control: 'boolean' },
  },
};

export const Default = {};

export const Ghost = {
  args: { variant: 'ghost', label: 'Cancel' },
};

HTML templates work the same way: import the file and point component at it.

// components/badge.stories.js
import html from './badge.html';

export default {
  title: 'Components/Badge',
  component: html,
  args: { tone: 'neutral', label: 'In review' },
  argTypes: {
    tone: { control: 'select', options: ['neutral', 'accent', 'warning'] },
  },
};

args are story defaults. argTypes declare controls (text, select, boolean, number) and can provide fallback prop metadata with description and type. If argTypes is omitted, the CLI infers a control from each arg value. Named export const objects are variants; they inherit meta args. Code Usage is generated from the imported component name plus the current args.

Point the root CLI at a folder:

# schublade.toml
name = "My kit"
stories = "./components"
catalog = "./catalog.toml"  # optional fallback

catalog.toml still works. Existing examples keep using it. When both are present, discovered story files merge in; a matching id from a story file replaces the catalog entry. See examples/story-files/.

Examples

Edge-case catalogs live under examples/. They are real configs, not an in-root demo mode.

cd examples/empty-catalog
npx schublade serve --config ./schublade.toml
# or
./run.sh

Each run.sh / build.sh uses npx schublade when the folder is a standalone checkout (the mirrored schublade-org/examples-<name> repos). Inside this monorepo they fall back to cargo run --manifest-path ../../Cargo.toml so local CLI changes apply. --config loads that folder’s schublade.toml; catalog and stories paths are resolved next to it.

The root catalog stays the default demo (npx schublade serve / cargo run -- serve). See examples/README.md.

Example repo sync

examples/ in schublade-org/cli is the source of truth. On push to main (and via Actions → Sync example repos), .github/workflows/sync-examples.yml mirrors each folder:

| Folder | Mirror | | --- | --- | | examples/story-files | schublade-org/examples-story-files | | examples/empty-catalog | schublade-org/examples-empty-catalog | | … | schublade-org/examples-<NAME> |

The job creates a public repo if it does not exist, then force-pushes the folder contents to main.

Secret: EXAMPLES_SYNC_TOKEN

The workflow reads only the repository secret named EXAMPLES_SYNC_TOKEN. If that secret is missing or empty, the job fails immediately with an error that names the secret.

Do not store the PAT as GITHUB_TOKEN. GitHub Actions already injects an automatic token under that name; a custom secret called GITHUB_TOKEN is ignored, and the automatic token cannot create or push other repositories in schublade-org.

Create a personal access token (the token owner must be allowed to create repositories in schublade-org), then add it at Settings → Secrets and variables → Actions → New repository secret with the exact name EXAMPLES_SYNC_TOKEN.

Classic PAT

| Scope | Why | | --- | --- | | repo | Create repositories in the org and force-push examples-*. public_repo is not enough to create repositories. |

Fine-grained PAT

| Field | Value | | --- | --- | | Resource owner | schublade-org | | Repository access | All repositories (must include future examples-* repos) | | Administration | Read and write (create repos) | | Contents | Read and write (push) | | Metadata | Read (granted automatically) |

Org approval may be required before a fine-grained token can act on schublade-org.

Do not commit a token. This repository does not invent or ship credentials.

Publish the npm package

Keep package.json and Cargo.toml versions in lockstep. Tag the same version:

git tag v0.1.0
git push origin v0.1.0

.github/workflows/release.yml builds each target, publishes @schublade/cli-<os>-<arch> first (the Rust binary lives inside that package), then publishes the root schublade package. Publishing uses npm Trusted Publishing: GitHub Actions exchanges its OIDC identity for short-lived npm credentials, so the repository does not need an NPM_TOKEN. Public packages receive npm provenance automatically.

Before creating a release tag, add the same GitHub Actions Trusted Publisher to the npm settings of schublade and every @schublade/cli-* platform package:

| npm Trusted Publisher field | Value | | --- | --- | | Organization or user | schublade-org | | Repository | cli | | Workflow filename | release.yml | | Environment name | npm | | Allowed action | npm publish |

The platform packages are @schublade/cli-darwin-arm64, @schublade/cli-darwin-x64, @schublade/cli-linux-arm64, @schublade/cli-linux-x64, @schublade/cli-win32-arm64, and @schublade/cli-win32-x64. Trusted Publisher settings are package-specific, so all seven packages must be configured. The workflow runs on a GitHub-hosted runner with id-token: write, Node 24, and npm 11.5.1 or newer.

After one successful OIDC release, remove any old NPM_TOKEN repository secret, revoke the corresponding npm automation token, and set each package's npm publishing access to Require two-factor authentication and disallow tokens. GitHub Release archives remain a convenience for non-npm installs; npx schublade installs the platform packages from npm.

What you get

  • CLI + server — clap + axum, published as the schublade npm package. The workshop chrome is a React app (Base UI + Tabler icons) embedded in the binary. In this repo, npm run dev:workshop iterates that chrome with mocks (Vite; no Rust). npm run build:workshop writes committed ui/chrome/. npx schublade serve embeds that output and hot-reloads catalog and stories; npx schublade build writes the same workshop as static HTML, including the configured favicon and logo. Consumers never run Vite.
  • Brand — logo and favicon in schublade.toml. The catalog name and mark are written into index.html before first paint so the wordmark does not flash “Schublade”.
  • Story discovery — *.stories.jsx / *.stories.js next to components (TOML still works), plus catalog.toml as fallback.
  • React preview — /api/render returns the imported component source and current props. The iframe mounts React from vendored UMD plus a small local JSX transform. No npm toolchain in the consumer repo.
  • Isolated iframe preview — the canvas is a sandbox="allow-scripts" iframe. The shell talks to it with postMessage only.
  • Native light/dark — configurable trigger in schublade.toml: data-attribute, class-name / className, or local-storage / localStorage.
  • Native a11y — a small DOM checker in the preview iframe. Enable, disable, or drop rules in schublade.toml.
  • AGENTS.md — one renderer writes GET /AGENTS.md, GET /{id}/AGENTS.md, and the same paths from schublade build. Content is catalog usage + controls, not source dumps or hash URLs.
  • Demo stories — Accordion, Avatar group, Badge, Button (Default / Ghost / Disabled), Chip.
  • Docs / tokens — *.mdx pages for color scales, typography, and other token tables. Tokens come from schublade.toml (manual) and/or a Tailwind theme CSS file. CSF + catalog.toml stay the story source of truth.

Configure

schublade.toml (loaded from the working directory, or --config):

catalog = "./catalog.toml"
stories = "./components"   # optional; walk for *.stories.js(x) / *.stories.toml
docs = "./docs"            # optional; walk for docs/token *.mdx pages
logo = "./logo.svg"        # optional workshop wordmark
favicon = "./favicon.svg"  # optional; copied into `schublade build` output

# Token adapters 3 (manual) and 4 (Tailwind CSS). 1 Figma MCP and 2 Paper MCP are sketched only.
[tokens]
css = "./tokens.css"

[[tokens.colors]]
name = "Ink"
id = "ink"
steps = { 50 = "#f8fafc", 900 = "#0f172a" }

[theme]
trigger = "data-attribute" # or "class-name", "local-storage"
key = "data-theme"
light = "light"
dark = "dark"

[a11y]
enabled = true
rules = ["image-alt", "button-name", "link-name", "label", "control-name"]

Adapter 4 is Tailwind-only. It classifies known @theme prefixes and ignores unknown --foo names. There is no --font-size-* prefix — font size is --text-*. --text-* is also overloaded (size, color, align/wrap skip, --text-shadow*). The prefix map:

| Prefix | Family | | --- | --- | | --color-* | colors ({palette}-{step} when the last segment is numeric) | | --text-* | typography size, text color, or skip | | --text-*--line-height | pairs with a --text-* size | | --text-shadow* | text-shadow | | --font-* | font family | | --font-weight-* | font-weight | | --leading-* | line-height | | --tracking-* | letter-spacing | | --tab-size-* | tab-size | | --spacing* | spacing | | --radius* | radius | | --shadow-* | box-shadow | | --inset-shadow-* | inset-shadow | | --drop-shadow-* | drop-shadow | | --blur-* | blur | | --perspective-* | perspective | | --zoom-* | zoom | | --aspect-* | aspect-ratio | | --ease-* | easing | | --animate-* | animation | | --breakpoint-* | breakpoints | | --container-* | containers |

See src/css_tokens.rs and examples/tailwind-tokens/.

A relative catalog or stories path is resolved against the config file’s directory. serve reloads catalog, stories, and theme/a11y config in place; host/port changes still need a restart. The Avatar group story uses a built-in renderer; HTML stories interpolate {{control}} tokens.

An empty catalog (name only, no [[stories]] and no story files) is valid — the workshop shows an empty state instead of refusing to start.

Layout

| Region | Role | | --- | --- | | Left | Catalog mark + docs pages and grouped story drawers (Button / Ghost → Button → Ghost) | | Center | Sandboxed canvas | | Right | Controls for the selected story, with a divider under the description | | Bottom | Inspector block: Code Usage and a11y tabs; breakpoints and light/dark as segmented controls |

Develop

Two loops. Consumers still need no JS toolchain — Vite and npm here are repo-author only.

Chrome (Vite + mocks)

Iterate the workshop React chrome with mock catalog / story / control / a11y data. No Rust.

npm install
npm run dev:workshop

Opens http://127.0.0.1:5173. Sidebar, inspector, controls, viewport, and theme run against workshop/dev/ mocks. This does not replace catalog or story hot-reload in schublade serve.

Integration (serve)

Compile chrome into the files the CLI embeds, then run the real server (or Cargo):

npm run build:workshop   # writes committed ui/chrome/ via esbuild — serve does not run Node or Vite
schublade serve
# or
cargo run -- serve

serve still watches catalog.toml and story files and reloads over /api/events. Use that loop to test the real catalog, not Vite.

CLI tests

Rust 1.85 or newer (rust-toolchain.toml pins 1.85.0). Node 18+ is only required to exercise the npm wrapper and this repo’s chrome tooling.

cargo test
npm install
npm run test:npm
cargo run -- serve --config examples/story-files/schublade.toml
cargo run -- build --config examples/story-files/schublade.toml --out dist/story-files
npx schublade --help