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

wvvy-player-widget

v0.2.0

Published

Embeddable live-radio player widget for any AzuraCast station. Single self-contained script, Shadow DOM isolated.

Downloads

455

Readme

Built for WVVY 96.7 LPFM, Martha's Vineyard community radio — but the station is just a default. Point it at any AzuraCast install.

Quick start

Paste this where the player should appear:

<div id="wvvy-player" data-variant="bar" data-frequency="96.7"></div>
<script src="https://wvvy.org/widget/player.js" async></script>

For another station, point it at that install:

<div
  id="wvvy-player"
  data-station="https://radio.example.org"
  data-shortcode="mystation"
  data-variant="card"
  data-theme="modern"
></div>
<script src="https://wvvy.org/widget/player.js" async></script>

Why not an iframe?

Two reasons that matter for a player specifically:

  • Lock-screen controls. Browsers bind navigator.mediaSession — the lock screen, notification shade, and CarPlay transport — to the top-level document. From a cross-origin iframe, none of it surfaces. Running in the host page means play/pause and track metadata reach the OS.
  • No height handshake. No fixed height="120", no postMessage resize dance. The widget sizes to whatever container it's dropped in, using container queries rather than viewport ones.

The usual argument for iframes is CSS isolation. That's covered here by rendering into a shadow root, which is stricter than an iframe in one direction: host page CSS cannot reach in, and the widget's CSS cannot leak out. Theming goes through documented custom properties, which are the one channel deliberately left open.

An iframe fallback still ships for site builders that strip <script>.

Variants

| Variant | Shape | | ------- | ------------------------------------------------------------------------------ | | bar | Single strip — play button, status line, current track, optional link. | | card | Full console — FM dial, transport, volume, now playing, today's schedule. |

Themes

| Theme | Look | | ----------- | ------------------------------------------------------------------------ | | brutalist | wvvy.org's house style: ink black, lime accent, square corners, stencil. | | modern | Softer dark card: navy ground, amber accent, rounded corners, system sans. |

Both accept an accent override, so modern + your brand colour is usually the fastest route to something that looks intentional on someone else's site.

Options

Every option is a data-* attribute on the container, or a key on the config object passed to mountPlayerWidget.

| Attribute | Default | What it does | | -------------------- | -------------------------- | ------------------------------------------------------------------------- | | data-station | https://radio.wvvy.org | Base URL of the AzuraCast install. | | data-shortcode | wvvy | Station shortcode within that install. | | data-variant | bar | bar or card. | | data-theme | brutalist | brutalist or modern. | | data-accent | theme default | Any CSS colour. Drives play button, on-air dot, EQ, dial needle. | | data-stream | from API | Explicit stream URL. Defaults to the station's own default mount. | | data-name | from API | Station name — card header and bar status strip. | | data-tagline | from API | Headline beside the card's play button. | | data-description | from API | Small line under that headline. | | data-location | — | Location line under the card header. | | data-frequency | — | e.g. 96.7. Also places the dial needle. | | data-link | — | Outbound link URL. Omit to hide the button. | | data-link-label | Full player and schedule → | Label for that link. | | data-show-dial | true | FM dial on the card. Requires data-frequency. | | data-show-schedule | true | Today's schedule strip. Hides itself if the station publishes none. | | data-schedule-url | station's AzuraCast | Fetch the schedule from here instead. Must return AzuraCast's schedule JSON and send CORS headers. | | data-show-listeners| false | Live listener count. Off by default; hides itself anyway if unpublished. | | data-show-volume | true | Volume slider. Always hidden on iOS. | | data-poll-interval | 20 | Metadata poll seconds. Floor of 10. | | data-fonts | auto | auto self-hosts the web fonts; none uses system fonts, zero requests. |

Unknown or malformed values log a warning and fall back to the default rather than failing to render. station, stream, and link accept only http(s) URLs — a javascript: URL in host-page markup is rejected.

Schedule from somewhere other than AzuraCast

The card's schedule strip reads AzuraCast's /api/station/{shortcode}/schedule. Plenty of stations don't keep their schedule there — WVVY's lives in a Google Sheet the station manager edits.

Rather than teach the widget about each station's storage, point it at any URL that returns the same JSON shape:

<div id="wvvy-player"
  data-variant="card"
  data-schedule-url="https://wvvy.org/api/schedule"></div>

Each entry needs name (or title) and start_timestamp; description in AzuraCast's "Streamer: <name>" form supplies the presenter, and is_now marks the current show:

[
  {
    "id": 58,
    "type": "streamer",
    "name": "Sunday Sessions",
    "title": "Sunday Sessions",
    "description": "Streamer: Ricky Prime",
    "start_timestamp": 1786892400,
    "start": "2026-08-16T12:00:00-04:00",
    "end_timestamp": 1786899600,
    "end": "2026-08-16T14:00:00-04:00",
    "is_now": false
  }
]

The endpoint must send Access-Control-Allow-Origin for the sites the widget runs on. WVVY's implementation — a Cloudflare Pages Function projecting a Google Sheet's recurring weekly rows onto dated occurrences — is in the wvvy.org repo at functions/api/schedule.ts.

Theming

Set custom properties on the container. They cross the shadow boundary by design:

#wvvy-player {
  --wvvy-accent: #f0a500;
  --wvvy-bg: #101820;
  --wvvy-panel: #18222c;
  --wvvy-raised: #22303c;
  --wvvy-text: #eef2f7;
  --wvvy-muted: #8fa0b3;
  --wvvy-line: #2c3a4b;
  --wvvy-live: #c4452f;
  --wvvy-warn: #f0a500;
}

npm

npm install wvvy-player-widget
import { mountPlayerWidget } from 'wvvy-player-widget';

const widget = mountPlayerWidget(document.getElementById('player')!, {
  station: 'https://radio.wvvy.org',
  shortcode: 'wvvy',
  variant: 'card',
  frequency: '96.7'
});

// on teardown
widget.destroy();

Bundler builds can't auto-detect where the fonts are served from. Either set assetBase to the URL you host dist/fonts/ at, or pass fonts: 'none' and let the system stacks take over.

Iframe fallback

Squarespace, Wix, and Google Sites strip <script> from user content on some plans. If yours does:

<iframe src="https://wvvy.org/widget/embed.html?variant=bar&frequency=96.7"
        width="100%" height="104" style="border:0" title="WVVY 96.7 live"></iframe>

It loads the same bundle and takes the same options as query parameters. The page posts a wvvy-player:height message to the parent for hosts that want to self-size. What you give up is lock-screen and CarPlay controls — see above.

Behaviour worth knowing

  • Multiple widgets per page are fine. Starting one stops the others, so nobody hears two offset copies of the same stream.
  • Polling is conservative. It pauses while the tab is hidden and while the widget is scrolled out of view, never runs faster than a 10s floor, and never stacks requests. This runs on someone else's page against someone else's server.
  • The metadata feed failing never claims the stream is dead. A hiccup marks the data stale and keeps the last known track; play stays available throughout.
  • Dropped streams reconnect with backoff (~35s across six attempts), then stop and say the station appears to be off air rather than spinning forever.
  • No cookies, no analytics, no third-party requests. Only the station's own API and audio mount. Fonts are self-hosted alongside the bundle — the widget never calls Google Fonts from a visitor's browser.

Development

npm install
npm run dev      # demo + snippet builder at localhost:5173
npm run all      # format → lint → test → build

npm run build emits:

| File | What it is | | ------------------ | ------------------------------------------------ | | dist/player.js | IIFE drop-in bundle, auto-mounts from data-*. | | dist/module.js | ESM entry for npm consumers, plus module.d.ts. | | dist/index.html | Demo and snippet builder. | | dist/embed.html | Iframe fallback page. | | dist/fonts/ | Self-hosted woff2 subsets (OFL 1.1). |

One sharp edge

Tailwind's @property registrations are document-scoped — a rule inside a shadow root's stylesheet is ignored outright. Without a workaround, border-2 computes to border-style: none, -translate-x-1/2 stops translating, and shadows silently vanish. src/lib/mount.ts hoists every registration's initial-value onto :host, where ordinary inheritance restores it. src/tests/unit/shadow-css.test.ts guards it, because the failure mode is silent rather than loud.

Licence

MIT. Bundled fonts are OFL 1.1 — see public/fonts/README.md.