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

@askelo/angular

v0.1.1

Published

Angular components, a directive and a signal-based service for the Askelo support widget.

Readme

@askelo/angular

The Askelo support widget for Angular: a floating chat launcher, an embedded chat for a support page of your own, and a signal-based service to drive both.

Standalone components and a directive — no NgModule. SSR-safe: nothing loads on the server, so an Angular Universal / @angular/ssr app needs no guard of its own.

npm install @askelo/angular

Quick start

Configure it once, in app.config.ts:

import { provideAskelo } from "@askelo/angular";

export const appConfig: ApplicationConfig = {
  providers: [provideAskelo({ widgetId: "your-widget-id" })],
};

That is the whole installation — the launcher appears in the corner of every route. Colour, position, greeting and handoff labels are resolved from the widget id on our side, so changing them in the dashboard never means changing your app.

Prefer to scope it to part of the UI? Skip provideAskelo() and drop the component in a template instead:

<askelo-widget widgetId="your-widget-id" />
import { AskeloWidgetComponent } from "@askelo/angular";

@Component({ imports: [AskeloWidgetComponent], /* ... */ })

Mounted in one route's template, the widget leaves with that route. Mounted in app.html, it is everywhere — the same as provideAskelo().

Controlling the widget

Inject AskeloService anywhere. Everything it exposes is a signal, so a template reads it directly:

import { AskeloService } from "@askelo/angular";

@Component({
  template: `
    <button (click)="askelo.open()" [disabled]="!askelo.isReady()">
      Support
      @if (askelo.unreadCount() > 0) {
        <span class="badge">{{ askelo.unreadCount() }}</span>
      }
    </button>
  `,
})
export class HelpButton {
  protected readonly askelo = inject(AskeloService);
}

| Signal | | | --- | --- | | status() | "idle" · "loading" · "ready" · "error" | | isReady() | shorthand for status() === "ready" | | error() | the AskeloError, in the "error" status | | isOpen() | whether the panel is on screen | | unreadCount() | agent replies since the visitor last had it open | | widget() | the underlying @askelo/browser handle, or null |

Methods: open(), close(), toggle(), identify(user), setTheme(theme). open() before the widget is ready is dropped rather than queued — a visitor who clicks Support during a slow load, gives up and navigates away should not have a panel open itself on the next page.

Your own launcher

Turn the built-in one off and wire up a button of your own:

provideAskelo({ widgetId: "your-widget-id", hideLauncher: true });
<button askeloLauncher class="my-support-button">
  Support
  @if (askelo.unreadCount() > 0) {
    <span class="badge">{{ askelo.unreadCount() }}</span>
  }
</button>

[askeloLauncher] brings no styles and no content — what a support trigger should look like is your decision. What it does bring is the part that is easy to get wrong: aria-expanded tracking the panel, aria-haspopup="dialog", an accessible name that mentions unread replies without overriding the button's own visible text, and disabled until the widget is ready (set [disableUntilReady]="false" to style the pending state yourself).

hideLauncher also suppresses the greeting bubble and the unread badge, since both are positioned against the launcher's corner. The unread count still arrives — that is how your own button carries its own badge.

A chat on your own page

<askelo-chat />

The same widget the launcher opens, drawn as a block instead of a card. That is the point rather than an implementation detail: a visitor who asks here and later opens the bubble on another page is in one conversation, and the agent who picks it up sees all of it.

Inside an app that called provideAskelo() it needs no attributes at all, and leaves the floating launcher exactly as that app configured it. Give it a widgetId of its own and it becomes the thing configuring the widget on that page — and then turns the launcher off, since a page that has embedded the conversation already has a way in:

<askelo-chat widgetId="your-widget-id" />
<!-- or keep the bubble too -->
<askelo-chat widgetId="your-widget-id" [showLauncher]="true" />

Sizing. The chat fills the element and brings no height of its own, so the element ships with one — 640px, the height of the floating panel. Override it with the custom property, or with any rule of yours that beats a component style:

askelo-chat {
  --askelo-chat-height: 100%;
}

A container of zero height renders a loaded, bootstrapped, invisible chat, which looks exactly like a widget that failed to load. The element carries data-askelo-chat="idle | loading | ready | error" if you want to style the wait.

Styling

Two mechanisms, split by which document the pixels are in.

The chrome is CSS. The launcher, the floating card and the greeting bubble are drawn in your page's DOM, so they read --askelo-* custom properties you set anywhere:

:root {
  --askelo-accent: #ff4f00;
  --askelo-launcher-size: 64px;
  --askelo-z-index: 900;
}

Media queries, dark-mode classes and scoped overrides all work, because it is ordinary CSS.

The panel is a message. It is a cross-origin iframe, so no cascade reaches it. Three values travel across:

<askelo-widget
  widgetId="your-widget-id"
  [theme]="{ accent: '#ff4f00', fontFamily: 'Inter, sans-serif', colorScheme: 'dark' }"
/>

colorScheme defaults to "auto" (the visitor's OS), which is the wrong answer for a dark-designed site whose visitor is set to light. Nothing is downloaded for fontFamily — name a face your page has already loaded.

Re-binding the theme re-themes in place; it never reloads the widget, and an inline object literal is safe (only its contents are compared).

Telling the widget who your visitor is

<askelo-widget widgetId="your-widget-id" [user]="{ externalId: user.id, email: user.email, name: user.name }" />

or, from a sign-in callback:

await this.askelo.identify({ externalId: user.id, email: user.email });

It prefills the contact details on a support case and names the visitor to the agent who picks it up. It is not authentication and it never widens what the widget can see.

Changing it re-identifies and never closes an open conversation — which matters most at exactly the moment it changes, since somebody logging in mid-chat is the ordinary case. An empty claim ({}, while your session loads) is a no-op rather than "this visitor is now anonymous".

If the widget has identity verification switched on, send a signature alongside — HMAC-SHA256(key = your widget's secret, message = externalId || email), hex, lowercase, computed on your server. A browser that can read the secret can claim to be any of your customers. Only that one field is proven: with an externalId, email rides beside it unsigned.

To prove both fields together, sign the versioned message instead and send signatureVersion: 2:

// on your server
import { createHmac } from "node:crypto";

function v2Message(externalId: string, email: string) {
  const normalizedEmail = email.trim().toLowerCase();
  return `v2\nuid:${externalId.length}:${externalId}\nemail:${normalizedEmail.length}:${normalizedEmail}`;
}

const signature = createHmac("sha256", process.env.ASKELO_IDENTITY_SECRET)
  .update(v2Message(user.id, user.email))
  .digest("hex");
await this.askelo.identify({ externalId: user.id, email: user.email, signature, signatureVersion: 2 });

Both fields are length-prefixed in the message so that no value either one can contain — a newline, a colon — can shift where uid ends and email begins. Existing integrations that sign only externalId or only email keep working unchanged.

Server-side rendering

Nothing to do. The service checks PLATFORM_ID and does not load on the server, so provideAskelo() in a shared app.config.ts is correct as written and <askelo-chat> renders an empty, correctly-sized box during hydration.

What your origin has to be

Your app's origin must be on the widget's allowlist in the dashboard — including http://localhost:4200 while you are developing. This is the same rule as the <script> tag; installing from npm does not exempt you from it.

API

provideAskelo(config)

widgetId · cdnUrl · hideLauncher · timeoutMs · theme · user · enabled.

<askelo-widget>

The same fields as inputs, all optional; anything you bind wins over provideAskelo(). Rendering the component opens a standing contribution and destroying it withdraws that one — so two surfaces on a page (a shell and a support route, say) merge field by field instead of erasing each other, and the one that leaves takes only its own settings with it.

<askelo-chat>

widgetId · cdnUrl · showLauncher · timeoutMs · theme · user · enabled.

[askeloLauncher]

On a <button>. One input: disableUntilReady (default true).

AskeloService

The signals and methods above.

Errors

error() holds an AskeloError with a machine-readable code: invalid_widget_id, invalid_cdn_url, script_load_failed, bootstrap_failed, timeout, no_browser, invalid_container, inline_unsupported, destroyed. A rejected origin arrives as bootstrap_failed.

Notes

  • One widget per (CDN origin, widget id), ref-counted. Two surfaces asking for the same widget get the same one, and neither can tear it out from under the other.
  • A widget already running on the page — from the pasted <script> snippet — is adopted rather than replaced, so a migration from the tag to this package is not a moment with two launchers or none.
  • Reloading only happens for widgetId, cdnUrl and timeoutMs. Everything else is applied to the running widget, because a reload closes the visitor's open conversation.
  • For a framework this package does not cover, @askelo/browser loads the same widget from any browser app.

MIT