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

@develemit/demo-kit

v0.1.1

Published

Shared harness for recording narrated Playwright walkthroughs for develemit-hq's Media Studio: caption timing (SRT, WebVTT and a read-aloud voice script, all from the same take), pacing, a synthetic cursor, focus rings, an optional on-screen narration pan

Readme

@develemit/demo-kit

Shared harness for recording narrated Playwright walkthroughs for develemit-hq's Media Studio: caption timing (SRT, WebVTT and a read-aloud voice script, all from the same take), pacing, a synthetic cursor, focus rings, an optional on-screen narration panel, a Playwright config preset, and the teardown that files the finished recording next to its captions.

The SRT output is pinned to develemit-hq's parser by its demo-kit-contract.test.ts, because a timing bug here shows up only as subtle audio/video drift in a finished video.

Entry points

  • @develemit/demo-kit — the runtime (everything below).
  • @develemit/demo-kit/config — defineDemoConfig, the Playwright config preset.
  • @develemit/demo-kit/teardown — the preset's globalTeardown. defineDemoConfig wires it in; you don't import it.

Config

// playwright.demo.config.ts
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineDemoConfig } from "@develemit/demo-kit/config";

const HERE = dirname(fileURLToPath(import.meta.url)); // CommonJS: __dirname

export default defineDemoConfig({
  repoRoot: resolve(HERE, "../.."),
  testDir: "apps/web-e2e/demo",
  testMatch: "**/*.demo.spec.ts",
  baseURL: "http://localhost:3000",
  basename: process.env.DEMO_BASENAME ?? "myproject-walkthrough",
  mediaDir: process.env.DEMO_OUTPUT_DIR ?? "media-studio",
  // optional: viewport (default 1920×1080), webServer, globalSetup, timeout
  // (default 15 min), outputDir (default .demo-results), and extra `use`
  // options such as { launchOptions: { slowMo: 150 } }
});

One worker, no retries (a retry records a second, conflicting take), video on at the viewport size, every path resolved from repoRoot. It sets DEMO_BASENAME, DEMO_KIT_MEDIA_DIR and DEMO_KIT_OUTPUT_DIR, which the runtime and teardown fall back to, so specs rarely pass paths. mediaDir may not sit inside outputDir, which Playwright empties every run.

Three ways to time a take

All three measure the take as it runs, never from a plan, so the captions match the video the same run produced.

1. A cue list — recordCues

Each cue's dwell comes from its word count (or dwellMs). Best for a new demo.

import { test } from "@playwright/test";
import { recordCues, type Cue } from "@develemit/demo-kit";

const CUES: Cue[] = [
  { text: "This is the dashboard.", run: async (page) => { await page.goto("/"); } },
  { text: "Every figure here is synthetic.", focus: (page) => page.getByTestId("kpi-total") },
];

test("walkthrough", async ({ page }) => {
  const clockStartedAt = Date.now(); // the page fixture's video is already rolling
  await recordCues(page, CUES, {
    title: "My Project",
    clockStartedAt,
    annotate: process.env.DEMO_ANNOTATE === "1",
    tailMs: 1200,
  });
});

| Option | Default | | |---|---|---| | basename, mediaDir | from defineDemoConfig | output file stem and directory | | annotate | false | ring each cue's focus; the ring's time comes out of the dwell, so an annotated take keeps the plain take's cue spans (variant adoption depends on it) | | cursor | annotate | draw the synthetic cursor | | narration | false | show each cue (with optional segment/title) in the on-screen narration panel | | pacing | 155 wpm, 650ms pad, 2200ms floor | { wordsPerMinute, padMs, minMs } | | clockStartedAt | when recordCues is called | epoch ms the video started; anything the test did before the call otherwise shifts every caption early | | captionEnd | "dwell" | "next-cue": a caption runs until the next cue starts, the last to the end of the take | | tailMs | 0 | hold on the final frame | | requireVideo | true | fail if the page isn't recording | | straightQuotes | false | write straight quotes, for consumers that mangle curly ones |

2. Free-form — createCaptionLog

For specs that pace themselves (fixed beats, interactions of unknown length). Each caption runs until the next mark.

const log = createCaptionLog({ clockStartedAt });
log.mark("Credentials are server-side configuration.");
await page.waitForTimeout(12_000);
log.mark("Tokens are cached until they expire.");
// ...
log.write({ title: "Code walk", straightQuotes: true });

3. Planned windows — createTimeline

Segments with planned start/end times, captions at offsets inside each, and highlights fired on schedule while the segment's actions run. Planned windows keep repeated takes (before/after a redesign) aligned segment by segment; an overrun is reported and the captions still follow the video.

const { context, page, videoStartedAt } = await openRecordedPage(browser, { baseURL, viewport, storageState });
await page.goto("/control-tower");
const timeline = createTimeline(SEGMENTS, {
  videoOffsetMs: Date.now() - videoStartedAt,
  runHighlight: (h) => focusOn(page, page.getByTestId(h.testId), h.testId, { moveCursor: false }),
});
await timeline.beat("control-tower", async () => { /* actions */ });
const { timings } = timeline.writeArtifacts({ title: "Demo", vtt: true, timingsJson: true, narrationScript: true });
await context.close(); // finalises the video; the teardown files it

Building blocks

  • warmUpOffCamera(browser, contextOptions, run) — sign in and compile routes in an unrecorded context; returns storageState.
  • openRecordedPage(browser, options) — a recording context for ({ browser }) specs, video into the teardown's directory, cursor installed; returns { context, page, videoStartedAt }.
  • installDemoCursor(page, { travelMs, size, clickRing }) — Playwright's video has no pointer.
  • focusOn(page, locator, label, { durationMs, ring, moveCursor, timeoutMs }) — glide to and ring an element; a stale target fails the take.
  • glideScroll(page, px, { steps, stepDelayMs }) — scroll that reads as movement.
  • installNarrationPanel / showNarration / showNetworkActivity / clearNetworkActivity — on-screen narration and a mirrored API-traffic panel, for silent or rehearsal takes. Leave it off for Media Studio takes; HQ burns captions itself.
  • showCode(page, { file, startLine, endLine, caption }) — render a real source file with a highlighted band, for code walks. Refuses .env files.
  • warmRoutes(baseUrl, routes) — prefetch dev-server routes from a global setup.
  • buildSrt, buildVtt, buildVoiceScript, cueDurationMs, formatClock — the pure pieces.

Migrating a bespoke harness

| Bespoke piece | demo-kit | |---|---| | paths.ts, hand-written Playwright recording config | defineDemoConfig | | collect-recording.ts, or copying video.path() by hand | the preset teardown | | srt.ts, voice-script.ts, pacing.ts, cue loop | recordCues | | timeline.ts + script.ts segments (centraflow, martialops) | createTimeline; layoutSegments for duration-drafted scripts; writeArtifacts({ vtt, timingsJson, narrationScript }) for the extra files | | showHighlight(page, { testId }) | focusOn(page, page.getByTestId(id), id, { moveCursor: false }) | | warm context + recordVideo context | warmUpOffCamera + openRecordedPage | | demo-overlay.ts say / writeSrt(path, totalMs) (immigration-app) | showNarration + createCaptionLog, or recordCues({ narration: true, captionEnd: "next-cue" }) | | captureApiTraffic / clearNet | showNetworkActivity / clearNetworkActivity | | scroll glide helper, slowMo | glideScroll, config use.launchOptions.slowMo | | code-view.ts showCode (immigration-app) | showCode (refuses .env files) |

Only migrate on a new or revised script. A re-capture of an already-ingested demo must reproduce its cue spans for variant adoption, and moving to different pacing shifts them.

Rehearsing: DEMO_PACE

cueDurationMs multiplies every paced dwell by DEMO_PACE (default 1). Run DEMO_PACE=0.1 DEMO_OUTPUT_DIR=/tmp/rehearse to check locators and assertions in seconds. Never keep a rehearsal take.

The SRT and video must come from the same take

Re-recording shifts every cue. Never pair a .srt from one take with a .webm from another — it reads as a subtle sync bug, and nothing here can detect it.

Developing

pnpm test (unit), pnpm typecheck, and pnpm test:e2e, which builds the package and records two short real-browser takes against it.