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

@yaps/support-web

v2.0.0

Published

Support Yaps web widget: <yaps-support> custom element and script-tag build.

Readme

@yaps/support-web

The Support Yaps web widget: a launcher bubble with an unread badge and a chat panel (a floating card on desktop, a sheet on phones), in a Shadow DOM so your styles and ours never mix. Colours, font and corners come from your Appearance or the one you publish in the inbox.

Install

Script tag (the Support server serves the built file):

<script src="https://support.yaps.ai/widget.js" data-app-id="app_123" async></script>

| Attribute | | |---|---| | data-app-id | Your public App ID (required). | | data-launcher | bottom-right (default), bottom-left, or hidden to use your own button. | | data-base-url | API origin. Defaults to the origin widget.js was loaded from. | | data-language | Force a UI language (normally the browser's). | | data-log-level | off, error, warn (default), info, debug. |

Or from npm:

npm install @yaps/support-web
import YapsSupport from "@yaps/support-web";
YapsSupport.configure({ appId: "app_123" });

Or as an element, wherever you like:

<yaps-support app-id="app_123" launcher="bottom-left"></yaps-support>
<!-- Inline, filling its container (give it a height): -->
<yaps-support app-id="app_123" mode="inline" style="height: 640px"></yaps-support>
<!-- Another server (development): -->
<yaps-support app-id="app_123" base-url="http://localhost:8787"></yaps-support>

The element's app-id, base-url and launcher only apply when the SDK isn't configured yet.

Quick start

The script tag sets window.YapsSupport. Calls made before it loads can be queued:

<script>
  window.YapsSupport = window.YapsSupport || [];
  YapsSupport.push(["identify", { userId: "u_42", name: "Ada", email: "[email protected]" }]);
</script>
<script src="https://support.yaps.ai/widget.js" data-app-id="app_123" async></script>
<script>
  window.addEventListener("yaps-support:ready", (e) => {
    const Support = e.detail;
    document.querySelector("#help").onclick = () => Support.open();
    Support.onUnread((n) => (document.querySelector("#help-badge").textContent = n || ""));
  });
</script>

API

| | | |---|---| | configure({ appId, baseUrl?, launcher?, mount?, appearance?, languageOverride?, logLevel?, desktop? }) | Done for you by the script tag. desktop is for Tauri and Electron apps (below). | | identify({ userId, name, email }) / identify({ token }) | Labels, or a verified identity. | | setMetadata(record) | Replaces the whole dictionary (≤ 30 string entries). | | logout(), deleteData() | Sign-out; deletion throws SupportError on failure. | | open(), close(), toggle() (openChat/closeChat too) | Show or hide the panel. | | onUnread(cb), onChatOpen(cb) | Called now and on change; return an unsubscribe function. | | setLauncher("bottom-right" \| "bottom-left" \| "hidden") | Move or hide the bubble. | | appearance, usesRemoteAppearance, languageOverride | Theme and language (below). | | onEvent | chatOpened, messageSent, imageSent, fileSent, reviewOpened, paywallOpened, appPromoOpened, codeCopied, codeOpened, linkOpened, identityRejected… | | paywalls, onPaywall, paywallAvailability, availablePaywalls, refreshPaywalls() | Offer cards. | | screens, onScreen | Link cards that open a page or view in your app (below). | | track(name, { opensChat }) | Reports a moment for automations. Never throws. | | enablePush({ serviceWorkerPath }) | Web push (below). |

The panel accepts images and PDF/TXT/ZIP/LOG files from the + button, drag and drop, and paste. Images are re-encoded in the browser as JPEG ≤ 2,048 px and ≤ 1 MiB before upload.

Appearance

YapsSupport.appearance = {
  accentColor: { light: "#0F766E", dark: "#2DD4BF" },
  onAccentColor: "#FFFFFF",
  chatBackground: { light: "#FFFFFF", dark: "#0B0B0C" },
  fontFamily: "Inter",
  cornerStyle: "soft",
  title: { en: "Help", es: "Ayuda" },
};

Remote (published in the inbox) beats local, which beats the SDK default, field by field. Changes apply the next time the panel opens. Position and stacking are CSS custom properties on the element: --yaps-support-offset-x, --yaps-support-offset-y, --yaps-support-z, --yaps-support-badge.

Identity verification

Your server signs a JWT (HS256) with the app's identity secret containing user_id and exp; the page passes it to YapsSupport.identify({ token }). Never ship the secret to the browser. Verified customers share one conversation across their devices in this app (App ID), including its mobile versions if they use the same App ID. Refresh the token when onEvent reports identityRejected with identity_token_expired.

Web push

Turn on the web platform for the app in the inbox (that adds config.webPush.publicKey), serve yaps-support-sw.js from your origin (it's in dist/, or copy it from https://support.yaps.ai/yaps-support-sw.js), then call this from a click handler:

button.onclick = async () => {
  const result = await YapsSupport.enablePush({ serviceWorkerPath: "/yaps-support-sw.js" });
  // { ok: true } or { ok: false, reason: "denied" | "unsupported" | "not_enabled" | "failed" }
};

The widget subscribes with PushManager and sends { provider: "webpush", token: JSON.stringify(subscription) }. Already have a service worker? Add importScripts("/yaps-support-sw.js") to it; its handlers ignore pushes and clicks that aren't from Support. Clicking a notification focuses your page and opens the chat (or opens your site with #yaps-support).

Desktop apps (Tauri, Electron)

Desktop web views have no Web Push and may ignore window.open, so pass a desktop object and your app does those jobs. The device registers as macos, windows or linux:

import YapsSupport from "@yaps/support-web";
import { openUrl } from "@tauri-apps/plugin-opener";
import { sendNotification } from "@tauri-apps/plugin-notification";
import { getCurrentWindow } from "@tauri-apps/api/window";

YapsSupport.configure({
  appId: "app_123",
  launcher: "hidden",
  desktop: {
    openUrl,
    notify: ({ title, body }) => sendNotification({ title, body }),
    setBadge: (count) => getCurrentWindow().setBadgeCount(count || undefined),
  },
});

While the chat is closed the widget checks for replies every pollIntervalSeconds and calls notify once per new reply; checkForReplies() does it on demand. Review cards open the app's desktop review link (a Trustpilot page, say). See the desktop guide.

Paywalls

YapsSupport.paywalls = [{ id: "yearly_offer", title: "Yearly 40% off" }];
YapsSupport.onPaywall = (id) => openCheckout(id);
YapsSupport.paywallAvailability = async (id) => isEligible(id); // optional

Offer cards are tappable only when the paywall is available and onPaywall is set. Review cards open the store URL in a new tab.

Link cards and screens

Your team can send a link card: the message with one button, drawn like an offer card. Web pages (https:) open in a new tab (noopener); mailto:, tel: and sms: links open in place; any other scheme is refused. Cards that open the app's system settings are for native apps and always read "No longer available" here. Cards can also open a page or view in your app:

YapsSupport.screens = [{ id: "edit_profile", title: "Edit profile" }]; // ≤ 50; titles are for your team
YapsSupport.onScreen = (id) => router.push(`/${id}`);                 // the panel has already closed

With the script tag, queue them like any call: YapsSupport.push(["screens", [{ id: "edit_profile", title: "Edit profile" }]]). A screen card is tappable only when its screen is in screens and onScreen is set; otherwise it reads "No longer available". Clicking it closes the panel (not an inline <yaps-support mode="inline"> chat), then calls onScreen. Screens are reported to the inbox like paywalls.

| Event | Fields | |---|---| | linkOpened | messageId, target (url or screen), screenId?, linkId? |

linkOpened and the open receipt follow the open (the new tab was requested, or onScreen was called); if onScreen throws, neither is sent. http/https links in message text are clickable and open in a new tab; email addresses stay plain text.

App promo and code cards

Your team can promote another of your apps or send a discount code. Cards arrive resolved for the web, so there's nothing to set up.

  • App promo: the app's icon, name and pitch with Get it, which opens the web store listing in a new tab (noopener). If the customer already uses the app it says "You already have it" with no button; with no listing for the web only the message text shows.
  • Code: the code in a monospaced box with Copy and Redeem. Copy uses navigator.clipboard.writeText (secure contexts; iframes need allow="clipboard-write"); when that's refused it selects the code and says "Select the code to copy it". Redeem opens the campaign's link in a new tab (noopener). A code for iPhone or Android shows Copy and "Redeem on iPhone" / "Redeem on Android" without Redeem. Past its expiry date the code is struck through with "Expired" and no buttons, and a code on screen turns expired when the time comes.

| Event | Fields | |---|---| | appPromoOpened | messageId, url, promotedAppId | | codeCopied | messageId, campaignId | | codeOpened | messageId, campaignId |

Each click also sends the action receipt (open for Get it and Redeem, copy for Copy). The buttons are real buttons with the widget's focus ring, so Tab, Enter and Space work; screen readers hear the code spelled out ("Code: S T U D E N T 2 0") and an announcement when it's copied. The code stays left to right in right-to-left chats.

Development

npm run build -w @yaps/support-web   # dist/widget.js (IIFE), dist/index.js (ESM), dist/yaps-support-sw.js
npm test -w @yaps/support-web        # vitest + happy-dom
  • demo-mock.html: the widget against a stubbed server (demo/mock.js), on a page with deliberately hostile styles. Serve the folder (npx vite sdk/web) and open /demo-mock.html?open=1 (?cards=states&open=1 adds the installed, other-platform and expired card states, and link cards).
  • demo.html: the widget against the Worker on http://localhost:8787 (/demo.html?appId=app_…).

widget.js is about 52 KB gzipped; the build fails above 60 KB.