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

@hello-bill/sdk

v2.12.2

Published

Browser embed for the HelloBill onboarding flow. Vanilla TS Web Component with Shadow DOM (<hellobill-embed>) and programmatic HelloBill.init() API.

Downloads

15,776

Readme

@hello-bill/sdk

Browser embed for the HelloBill Partner API (Revision 1.13) (api_version 2026-05-10). Vanilla TS Web Component with Shadow DOM (<hellobill-embed>) and a programmatic HelloBill.init() API.

  • Single ESM file (npm) + IIFE bundle (script tag).
  • Shadow DOM (open) — fully isolated from host-page CSS.
  • 14-screen state machine with branching (anyvan + comparison gate on move data).
  • WCAG 2.2 AA contrast, axe-clean, keyboard-navigable, prefers-reduced-motion aware.

Install

npm install @hello-bill/sdk
# or via CDN (script tag, no build step):
# <script src="https://unpkg.com/@hello-bill/sdk/dist/index.global.js"></script>

Quick start (custom element)

<hellobill-embed
  endpoint="/api/hellobill"
  session-data='{"customer":{"first_name":"Alice","last_name":"Tenant","email":"[email protected]"},"addresses":{"current":{"address_line_1":"12 Elm Road","city":"London","postcode":"N7 8AA"}},"move":{"in":{"move_in_date":"2026-06-01"}},"consent":{"data_sharing_accepted":true}}'>
</hellobill-embed>
<script type="module">
  import '@hello-bill/sdk'; // registers <hellobill-embed>
</script>

mode attribute

The embed supports two boot modes:

  • mode="card" (default) — boots into the compact partner-home feature card preview. The embed sizes to its content. Tapping the inner CTA unfurls the full takeover (16-screen flow).
  • mode="full" — skips the preview and boots directly into the intro screen. Use this when the embed already lives inside a takeover (modal, drawer, dedicated route).
<hellobill-embed mode="card" ...></hellobill-embed>  <!-- default: feature-card preview -->
<hellobill-embed mode="full" ...></hellobill-embed>  <!-- skip card, open full flow -->

The host element gets a data-mode="card|full" attribute reflecting the current state — partners can style their wrapper accordingly. The card state is a one-way bootstrap: the user cannot navigate back to it via the in-flow back chevron.

Quick start (programmatic)

import { HelloBill } from '@hello-bill/sdk';

const handle = HelloBill.init({
  mountTo: '#hellobill-root',           // CSS selector or HTMLElement
  baseEndpoint: '/api/hellobill',       // your server-side SDK adapter
  sessionData: {                        // partner-side user/property data
    firstName: 'Sam',
    lastName: 'Lee',
    email: '[email protected]',
    addressLine1: '12 Elm Road',
    city: 'London',
    postcode: 'N7 8AA',
    moveInDate: '2026-06-01',
    bedrooms: 2,
    propertyType: 'flat',
  },
  onComplete: (result) => console.log('done', result),
  onClose: (reason) => console.log('closed', reason),
  onEvent: (event) => console.log('event', event),
});

// Later (or on partner-side cleanup):
// handle.destroy();

embedOrigin (local development)

embedOrigin defaults to https://embed-sandbox.hellobill.app — correct for production, where your live domain is on the embed's parent-origin allowlist. During local development the embed iframe is served from a different origin and your localhost page is not on the production allowlist, so the iframe will mount but never finish initialising. Point embedOrigin at your sandbox / local embed origin and ensure that origin's allowlist includes your dev host:

HelloBill.init({
  mountTo: '#hellobill-root',
  baseEndpoint: '/api/hellobill',
  sessionData,
  embedOrigin: 'https://embed-sandbox.hellobill.app', // or your local embed origin
});

Session data shape

interface SessionData {
  // Customer identity
  firstName: string;
  lastName: string;
  email: string;
  phone?: string;

  // Property address (flat camelCase)
  addressLine1: string;
  addressLine2?: string;
  city: string;
  postcode: string;
  bedrooms?: number;
  moveInDate?: string;         // ISO date — e.g. '2026-06-01'

  // Optional property hints
  propertyType?: 'detached' | 'semi_detached' | 'terraced' | 'flat' | 'bungalow' | 'maisonette' | 'other';

  // Previous address — triggers the comparison screen when present
  previousAddress?: { addressLine1: string; city: string; postcode: string; /* ... */ };

  // Move-out — triggers the AnyVan screen when present
  moveOut?: { move_out_date: string; /* ... */ };
}

The embed forwards this to your baseEndpoint + '/session' route; the SDK then shapes it into the upstream POST /partner/sessions payload.

Events

The embed emits typed events through onEvent and as CustomEvents on the <hellobill-embed> element (hb:*):

| Event | Fires when | |----------------------|-----------------------------------------------------------| | screen.entered | A new screen is shown. Payload: { screen: string }. | | screen.exited | A screen advances. Payload: { screen: string }. | | selection.changed | User toggles a product in pickers. | | loa.fetched | LoA documents loaded. Payload: { loa_ids, checksum }. | | loa.signed | All LoAs signed in this run. | | customers.created | POST /customers returned 2xx. | | error | Any embedded error reaches the user. Payload: { code, message }. | | hb:complete | Convenience alias; also invokes onComplete. | | hb:close | User dismissed (Esc or close button); invokes onClose. |

Branching

The state machine only renders the screens it has data for:

  • AnyVan moving-help screen — shown when move.out is present.
  • Previous-vs-new comparison screen — shown when addresses.previous is present.
  • Service pickers — gated on which categories come back from /products.

A simple move-in flow naturally collapses to ~9 screens; a full relocation expands to the 14-screen path.

Theming

Full theming reference: docs/theming.md

The embed ships with four theme presets, per-token variable overrides, and component-level CSS rules. All can be combined and switched live without re-mounting.

Theme presets

| Preset | Look | |---|---| | hellobill | Default. Deep-forest surface, mint accents, white text. | | light | White surface, dark forest ink, darker mint accent. | | dark | Identical to hellobill visually. Both names exist so system has a stable dark target. | | system | Resolves to light or dark from prefers-color-scheme on init and live-updates on OS change. |

<!-- Custom element -->
<hellobill-embed theme="light" endpoint="/api/hellobill" session-data="{...}"></hellobill-embed>
<hellobill-embed theme="system" endpoint="/api/hellobill" session-data="{...}"></hellobill-embed>
// Programmatic
const handle = HelloBill.init({
  baseEndpoint: '/api/hellobill',
  mountTo: '#hellobill',
  sessionData,
  appearance: { theme: 'light' },
});

// Live switch — re-cascades CSS vars, active flow stays mounted
handle.updateAppearance({ theme: 'dark' });

Token overrides (appearance.variables)

Override individual design tokens on top of the active preset:

HelloBill.init({
  appearance: {
    theme: 'light',
    variables: {
      colorPrimary: '#5B21B6',
      colorOnPrimary: '#ffffff',
      colorText: '#0f172a',   // secondary/placeholder/disabled tiers auto-derive from this
      borderRadius: '8px',
      buttonBorderRadius: '8px',
    },
  },
  ...
});

Text-tier cascade: setting colorText alone is enough for a coherent text hierarchy. The three sub-tiers (colorTextSecondary, colorTextPlaceholder, colorTextDisabled) automatically derive from colorText when not explicitly set, so muted text never becomes invisible. Explicit sub-tier values always override the derivation.

See docs/theming.md for the full token reference table, component rules, what is locked, and the iframe sizing contract.

Iframe sizing

An unsized mount (no explicit CSS height) starts at a 720px default and then dynamically grows or shrinks to fit the embed's content, clamped between 480px and either window.innerHeight or your own maxHeight option:

HelloBill.init({
  mountTo: '#hellobill-root',
  baseEndpoint: '/api/hellobill',
  sessionData,
  maxHeight: 900, // optional — defaults to window.innerHeight
});

If you size the mount yourself instead, the iframe fills that box at 100% and this dynamic behaviour does not apply — see docs/theming.md#iframe-sizing for the full contract.

The ceiling (window.innerHeight or maxHeight) is always the current viewport — a bare browser-window resize re-clamps the iframe even without a new hb:resize message, so the frame never exceeds 100vh. Content taller than that ceiling scrolls inside the embed rather than being clipped.

Browser support

Modern evergreen browsers — relies on native:

  • Custom Elements v1
  • Shadow DOM v1
  • ES2020 (globalThis, optional chaining, nullish coalescing)
  • fetch, AbortController

No IE / no legacy Edge. No polyfills required for the supported targets.

Accessibility

  • WCAG 2.2 AA contrast verified against design tokens (success colour bumped to #15803d for the cashback pill in P6).
  • Every screen passes axe-core (no serious / critical) via the Playwright harness in e2e/hellobill/.
  • All interactive primitives are keyboard-reachable; RadioCard handles Enter / Space, ErrorMessage announces via role="alert", signature canvas exposes role="img" + aria-label.
  • Esc dismisses the embed and emits hb:close with reason: 'user_dismissed'.
  • Animations and transitions disabled under prefers-reduced-motion: reduce.

Known limitation — session token expiry

The session token returned by POST /session is a short-lived JWT (300 s). If the user idles past 5 minutes mid-flow, the next API call returns 401 auth.token_expired and the embed surfaces the upstream error.

A future onSessionExpired hook will let the host page refresh and re-mint — this is a public-API change and is deferred to a dedicated ADR ("Embed session refresh"). See PUNCH_LIST.md → 🟡 Needs followup #1.

Build

bun run build           # tsup → dist/index.js (ESM) + dist/index.global.js (IIFE)
bun run build:measure   # build + gzip size report
bun run check:bundle    # gzip size report only (assumes dist/ exists)

Bundle size

Measured via bun run check:bundle (gzip level 9 against the tsup output).

| Format | File | Raw | Gzip | Budget | | ------ | ---------------------- | -------- | -------- | -------- | | ESM | dist/index.js | 76.07 KB | 17.13 KB | 50.00 KB | | IIFE | dist/index.global.js | 80.98 KB | 17.61 KB | 50.00 KB |

Budgets are enforced — check:bundle exits non-zero if any artefact exceeds 50 KB gzipped. Current usage: ~34 % of budget.

Tests

bun run test            # vitest — 34 / 34 passing
bun run typecheck       # tsc --noEmit

End-to-end coverage (Playwright + axe) lives in e2e/hellobill/ at the repo root — 28 visual baselines (desktop + mobile) committed.

Test seams (opt-in)

The custom element exposes a tiny escape hatch surface ONLY when the host page sets window.__hb_test_enabled = true BEFORE the element boots. In production this flag is never set, so the methods below do not exist and cannot be reached. The flag is unrelated to the build — the same bundle ships everywhere.

When enabled, each <hellobill-embed> instance gains:

| Method | Purpose | | --- | --- | | __hb_test_setLoaChecksum(loaId, newChecksum) | Replace the in-memory LoA checksum so the next POST /customers trips loa.template_changed. Pass '*' as loaId to patch every LoA. | | __hb_test_getScreen() | Current screen id ('intro', 'loa', …). | | __hb_test_getContext() | Structured-clone of MachineContext (read-only snapshot). |

Usage from a Playwright test:

await page.evaluate(() => {
  const el = document.querySelector('hellobill-embed');
  el.__hb_test_setLoaChecksum('*', 'tampered_value');
});

These exist for end-to-end testing of negative paths (LoA drift, mid-flow state inspection) and are documented to discourage accidental dependence in partner integrations.

License

MIT.