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

@beatzball/litro-native

v0.2.0

Published

Native device capabilities for web apps — haptics, local notifications, app badge, secure storage, share and review request — behind one interface with an honest web fallback for each. Server-render safe, zero dependencies. Works standalone or as part of

Readme

@beatzball/litro-native

Native device capabilities for a web app, with an honest fallback for each one.

import { haptics } from '@beatzball/litro-native/haptics';

haptics.impact('light');

That one call is a device buzz inside the native app, a vibrate in an Android browser, and nothing at all in iOS Safari, which has no Vibration API. Your code does not branch, and no call throws because a platform feature is missing.

A call can still reject when the platform itself refuses one: sharing outside a user gesture, or a storage write with no room left. Those are real failures and you get to see them.

Zero dependencies. Safe to import in a page that renders on the server.

Status

Phase 1: the web path. The interface, the bridge and the web fallbacks are here and useful on their own. The native host is not built yet, so every capability currently takes its web behavior.

Not published yet. This package is in the repository and not on the registry. The release pipeline authenticates per package, and that cannot be set up for a package that does not exist on the registry, so the first publish is a one-time manual step that has to happen before the automated one can.

Install

pnpm add @beatzball/litro-native

The capabilities

| Import | What it does | On the web | |---|---|---| | haptics | impact, notify, selection | Vibration API; silent in iOS Safari | | badging | sets or clears the app icon badge | Web Badging API, once installed | | share, canShare | opens the platform share sheet | Web Share API | | secureStorage | get, set, remove | browser storage — not a keychain | | notifications | schedules a local reminder | nothing; see below | | requestReview | asks for a store review | nothing | | platform, supports | what we are running on | reports the browser |

Each one also has its own subpath, so an app that wants haptics alone imports only that facade. The bridge behind them is one small module, loaded lazily on the first call, and it carries all six web implementations — so importing one capability defers the others rather than dropping them.

Namespace or function

The shape follows what the capability is, and it is worth knowing so you do not reach for the wrong name:

  • Several actions, so a namespace: haptics, notifications, badging, secureStorage.
  • One action, so a function: share, canShare, requestReview.

There is no review object. requestReview() is the whole surface.

Two things to read before you rely on it

Local notifications need the native host. A browser cannot run a timer for a page that is closed, so schedule does nothing and returns an empty id. Do not offer a reminder feature and hope. Ask first:

import { supports } from '@beatzball/litro-native/platform';

if (await supports('notifications')) {
  // Safe to show the reminder setting.
}

Secure storage is not secure on the web. The fallback is browser storage. Any script on the origin can read it and it shows up in developer tools. For anything that would matter if it leaked, such as a long-lived access token, check the platform first and keep the web build signed out:

import { platform, secureStorage } from '@beatzball/litro-native';

if ((await platform()).isNative) {
  await secureStorage.set('token', token);
}

set rejects if the write itself fails, which is almost always a full quota. Catch it if losing the value matters:

try {
  await secureStorage.set('draft', text);
} catch {
  // The old value is still there. Tell the user nothing was saved.
}

supports versus "safe to call"

These are different questions, and the difference matters.

  • Every capability is always safe to call. No call throws because of a missing platform feature. A platform that refuses the operation itself is a different thing, and does reject.
  • supports(name) says whether the call does anything. Use it to decide whether a feature belongs in the interface at all.

On the web the list is built from the APIs the browser actually exposes, so it differs between two browsers, and between two visits to the same browser.

Server rendering

Nothing native is imported or touched while a module here is evaluated. The bridge resolves on the first call, in the browser only, so a server render gets the web answer and never throws. If you add to this package, keep it that way: a browser global read at module scope breaks the server render, builds cleanly and fails silently.

Use it in a component

@beatzball/litro-native/lit exports a controller that reads the platform when the host connects and asks for one re-render when the answer lands.

import { LitElement, html } from 'lit';
import { NativeController } from '@beatzball/litro-native/lit';
import { haptics } from '@beatzball/litro-native/haptics';

class TapButton extends LitElement {
  private native = new NativeController(this);

  render() {
    if (!this.native.supports('haptics')) return null;
    return html`<button @click=${() => haptics.impact('light')}>Tap</button>`;
  }
}

supports() is false until the answer arrives, not true-by-default. So a control that needs a capability is absent during the server render and absent on the first client frame, instead of appearing and then vanishing. platform is null over the same window, and holds the full {@link PlatformInfo} after.

A read that fails is not cached. The next time the host connects, the controller tries again, because one failed chunk load should not hide every native capability for the life of the page.

This subpath does not import Lit. The host type is declared structurally, so a LitElement satisfies it with no cast, and the package depends on no Lit major version — or on Lit at all. Any component base class with addController and requestUpdate works the same way.

License

Apache-2.0