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

@theclearsky/easy-tutorial-builder

v0.0.2

Published

Interactive, fault-tolerant product tours as typed, data-only scripts: event-driven steps, pan/zoom-aware spotlight, remote launch over postMessage.

Readme


Why

Most tours are a list of "click Next" bubbles pinned to CSS selectors. They break the moment the user does something unexpected, the layout pans, or an element re-mounts. This library treats a tour as a small state machine your app already understands:

| | | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🎯 Steps advance on real actions | A step waits for an app event ("demo.loaded") or a condition — not a Next button. Clicks on the real target get through the spotlight. | | 🧭 Distractions route back | "The menu closed → go back to open the menu" is just a transition in the script. Timeouts show hints; an optional Show me assist does it for the user. | | 🔦 Follows moving targets | Targets are re-found every frame, so the spotlight keeps up with pan/zoom canvases (React Flow), re-mounted elements, portals, scrolling and fullscreen. Several holes at once keep submenus clickable. | | 🧾 Scripts are data | A tutorial only names things your app registered — targets, events, predicates, assists. Typed in TypeScript, validated with zod for JSON, so a script is safe to load from a docs site. | | 🎭 Your guide, your look | The library draws only the spotlight. Your speaker — a mascot, a bubble, a voice — renders from one getView() snapshot: lines, mood, target rect, buttons. | | 🌐 Remote launch | A docs page can open your app in a new tab and start a tutorial over postMessage: exact-origin allow-list, nonce, size cap, user consent. |

It takes ideas from the typed step model and failure reporting of React Joyride, and the framework-free, imperative control of driver.js.

Install

npm install @theclearsky/easy-tutorial-builder

The core has no dependencies and runs without React. react (18+) and zod (4) are optional peers, needed only by the entries that use them.

| Entry | What's in it | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | @theclearsky/easy-tutorial-builder | Core: defineKit, defineTutorial, createTutorial, highlight, createProgressStore, byTourId, the spotlight and tracker | | …/react | TutorialProvider, useTutorial, useTutorialView | | …/schema | parseTutorial, tutorialSchema, tutorialJsonSchema — validate JSON scripts (zod) | | …/remote | acceptRemoteTutorials, launchTutorial — start a tutorial sent from another tab |

Quick start

1. Describe your app: a kit

The kit is your app's vocabulary — everything a script is allowed to mention.

import { byTourId, defineKit } from "@theclearsky/easy-tutorial-builder";

export const kit = defineKit({
  targets: {
    "toolbar.demos": () => byTourId("toolbar.demos"), // <button data-tour="toolbar.demos">
    "menu.item": ({ id }) => document.querySelector(`[data-menu-item="${id}"]`),
  },
  events: ["menu.opened", "menu.closed", "demo.loaded"],
  predicates: { menuOpen: () => !!document.querySelector("[role=menu]") },
  assists: {
    openMenu: () =>
      document
        .querySelector<HTMLElement>('[data-tour="toolbar.demos"]')
        ?.click(),
  },
  capabilities: ["demosMenu"],
});

// Anywhere in your app, whether or not a tutorial is running:
kit.bus.emit("demo.loaded", { id: "piano" });

2. Write a tutorial: data only

{
  "format": 1,
  "id": "open-piano",
  "revision": 1,
  "title": "Open the piano",
  "requires": ["demosMenu"],
  "steps": [
    {
      "id": "demos",
      "type": "point",
      "target": { "name": "toolbar.demos" },
      "skipIf": { "pred": "menuOpen" },
      "lines": [{ "say": "Click **Demos**.", "mood": "pointing" }],
      "advance": [{ "on": { "event": "menu.opened" }, "goto": "next" }],
      "timeoutMs": 12000,
      "hint": [{ "say": "It's the purple button." }],
      "assist": { "name": "openMenu", "label": "Show me" }
    },
    {
      "id": "piano",
      "type": "point",
      "target": { "name": "menu.item", "args": { "id": "piano" } },
      "lines": [{ "say": "Now pick **Piano**." }],
      "advance": [
        {
          "on": { "event": "demo.loaded", "args": { "id": "piano" } },
          "goto": "next"
        },
        { "on": { "event": "menu.closed" }, "goto": "demos" }
      ]
    },
    {
      "id": "done",
      "type": "end",
      "lines": [{ "say": "That's it!", "mood": "celebrate" }]
    }
  ]
}

| Step | What it does | | -------- | ---------------------------------------------------------------------------------------------------------------------- | | say | The guide talks; the user presses Next. | | point | Spotlight a target and wait for an action: an event (on, with argument matching and counts) or a condition (when). | | choice | Offer buttons; each goes somewhere. | | branch | Jump on a condition, silently. | | end | Finish, with an outcome. |

Conditions are { pred }, { not }, { all } and { any } — no expressions, nothing to evaluate. In TypeScript, defineTutorial(kit, { … }) turns an unknown name into a type error; for JSON files, tutorialJsonSchema(kit) gives editors autocompletion.

3. Run it

import {
  createProgressStore,
  createTutorial,
} from "@theclearsky/easy-tutorial-builder";
import { parseTutorial } from "@theclearsky/easy-tutorial-builder/schema";

const parsed = parseTutorial(kit, jsonText, { maxBytes: 512_000 });
if (!parsed.ok)
  throw new Error(
    parsed.issues.map((i) => `${i.path}: ${i.message}`).join("\n"),
  );

const tour = createTutorial({
  kit,
  tutorial: parsed.tutorial,
  progress: createProgressStore(localStorage),
});
tour.subscribe(() => renderMyGuide(tour.getView())); // lines, mood, target rect, buttons
tour.on("failure", (failure) => console.warn(failure)); // target_not_found, assist_failed, …
tour.start(); // or tour.resume() from the last checkpoint

Recipes

React

import type { Tutorial } from "@theclearsky/easy-tutorial-builder";
import {
  TutorialProvider,
  useTutorial,
} from "@theclearsky/easy-tutorial-builder/react";

function TourButton({ script }: { script: Tutorial }) {
  const { view, start } = useTutorial();
  return view?.status === "running" ? null : (
    <button onClick={() => start(script)}>Show me around</button>
  );
}

<TutorialProvider kit={kit}>
  <App />
</TutorialProvider>;

Draw your own guide

Everything a speaker needs is in one immutable snapshot, refreshed on every change:

const view = tour.getView();
view.lines; // what to say now — [{ say, mood }]
view.hint; // extra lines once the step timed out
view.target; // the spotlighted element's rect (follows it every frame)
view.placement; // where you asked the bubble to sit
view.waitingForAction; // a point step: show "your turn…", no Next button
view.canNext;
view.canPrev;
view.options; // the buttons to render
view.assist; // { name, label } → tour.runAssist()
view.nudges; // bumps when the user clicks the dimmed page — wiggle your mascot

Spotlight one thing, no tour

import { byTourId, highlight } from "@theclearsky/easy-tutorial-builder";

const glow = highlight(() => byTourId("save"));
// later
glow.destroy();

Resume where they left off

createProgressStore(localStorage) records finished tutorials (per revision) and the last checkpoint: true step. tour.resume() starts from that checkpoint; bump a tutorial's revision when its steps change, and old checkpoints are ignored.

Launch from a docs site

// the app, in the FIRST module that runs (before any UI):
import { acceptRemoteTutorials } from "@theclearsky/easy-tutorial-builder/remote";
const remote = acceptRemoteTutorials({
  kit,
  allowedOrigins: ["https://docs.example.com"],
});
remote.onRequest((request) =>
  askTheUser(request).then((yes) =>
    remote.respond(request.requestId, yes ? "accept" : { reject: "declined" }),
  ),
);

// the docs site, inside a click handler:
import { launchTutorial } from "@theclearsky/easy-tutorial-builder/remote";
const result = await launchTutorial({
  appUrl: "https://app.example.com/",
  script,
});
// → accepted | rejected{reason} | blocked | no-opener | closed | timeout

The app speaks first. It announces ready only after its listener is installed, and repeats it; the docs site listens before it opens the app and only ever replies — no message is ever sent to a listener that doesn't exist yet.

The receiver checks that:

  • the sender's origin exactly matches the allow-list, and the sender is the window that opened the app;
  • a one-time nonce is used, one request per page load, within a 60 s window;
  • the size fits before parsing (512 KB by default), the script passes schema validation, and the app has every capability it requires;
  • the app is not inside a frame.

The request is queued in sessionStorage, so it survives reloads and a "click to start" gate. The app only ever sends back ready, accepted or rejected with a reason code.

API at a glance

| Export | What it does | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | defineKit(definition) | Your app's vocabulary: targets, events (with an event bus), predicates, assists, capabilities. | | defineTutorial(kit, script) | A script typed against the kit (unknown names are type errors). | | createTutorial(options) | The controller: start, resume, next, prev, goTo, choose, skip, runAssist, getView, subscribe, on('step' \| 'finish' \| 'failure' \| 'outside'), destroy. Options: spotlight (or false), outsideClick (block / pass / skip), dimWhileTalking, targetWaitMs, progress. | | highlight(find, options?) | A one-off spotlight that tracks its element. | | createProgressStore(storage) | Completion per revision + resume checkpoints. | | byTourId(id, root?) | The element with data-tour="id" (CSS-escaped), or null. | | parseTutorial · tutorialSchema · tutorialJsonSchema | Validate untrusted JSON with exact error paths and size limits; a JSON Schema for editors. | | acceptRemoteTutorials · launchTutorial | Remote launch, both ends. | | createSpotlight · createTracker · engineReducer | The building blocks, for custom renderers and tests. |

Built to be trusted

  • Framework-free core, zero dependencies, ~7 KB gzip; React and zod only where you import them.
  • 25 tests cover the engine's transitions (events with argument matching and counts, skipIf on entry and while waiting, branches, cycle guards), the controller, schema validation (unknown names, exact error paths, size limits) and the remote-launch handshake.
  • Used in production apps with pan/zoom canvases, portals and fullscreen video — the cases tours usually break on.
  • Releases after 0.0.1 are built and published by CI with npm provenance.

Used by

| Project | Guide | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | watch-together — watch local videos together, peer to peer, from a static page (source) | Pop, a popcorn bucket | | Nodestra — a node-based sound design app, where this library was born | Blip, a small conductor |

Its sibling, easy-folder-management-ui (npm), gives web apps a VS Code-style file library and tab strip; both libraries came out of the same apps.

License

MIT © 2026 Deepak Prasad