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

@dolard.eu/versiq-widget

v0.6.0

Published

Versiq Widget SDK - Embed conversational qualification into your website.

Readme

@dolard.eu/versiq-widget

npm version bundle size types license

Versiq Widget SDK — embed a conversational conversion agent that answers visitors using your site's data (catalogue, inventory, CRM, knowledge base), not the public-web average a generic LLM assistant returns.

Table of Contents

Installation

npm install @dolard.eu/versiq-widget
# or
pnpm add @dolard.eu/versiq-widget

Quick Start

Script Tag (CDN)

Hot-link the latest published bundle from any public npm CDN. A single line is enough — the publishable key binds the widget to your Agent, everything else is resolved server-side.

<script
  src="https://unpkg.com/@dolard.eu/versiq-widget@latest/dist/widget.umd.js"
  data-api-key="pk_your_key"
></script>

Equivalent via jsDelivr:

<script
  src="https://cdn.jsdelivr.net/npm/@dolard.eu/versiq-widget@latest/dist/widget.umd.js"
  data-api-key="pk_your_key"
></script>

@latest always serves the most recent release, so a bug fix or improvement reaches your visitors without you editing the snippet. If you need to pin a specific version for change-control reasons, replace @latest with @<VERSION> (see npm for the list) — at the cost of having to bump it manually on every release.

Programmatic API

The minimal integration is one line — every visual and behavioural aspect (theme, position, language, open-state, branding) is configured in the Versiq admin portal for your Agent and resolved server-side from the API key.

import { createWidget } from "@dolard.eu/versiq-widget";

const widget = createWidget({ apiKey: "pk_live_your_key" });

// Control the widget at runtime
widget.open();
widget.close();
widget.reset();

// Cleanup (e.g., on SPA route change)
widget.destroy();

Why this widget converts

Two reasons most platforms fail to convert mobile visitors, and two reasons this widget does:

  1. It speaks with your data, not the public-web average. The agent answers from your catalogue, your stock, your CRM, your knowledge base — what your competitors and generic AI assistants don't have.
  2. It is driven by your thumb, not your keyboard. The LLM dynamically picks the right component at every turn — quick replies, sliders, product cards — instead of forcing a form. Visitors qualify their need in a few taps on mobile, where 3+ field forms lose 50%+ of users (HubSpot).

The widget rendering layer (QuickReplies, PropertyCard, ActionButtons) is the runtime that materialises this. Schemas live in the Versiq backend repository and are out of scope for the SDK consumer.

Configuration

Widget configuration is split into three scopes — only the first one is your responsibility as an integrator.

1. Host-side (passed to createWidget or as data-* attributes)

These can only live on the integrator's page because they describe the host context (DOM, identity, environment).

| Option | Type | Default | Description | | ----------- | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------- | | apiKey | string | required | Publishable API key (pk_live_*, pk_test_*). Binds the widget to one Agent. | | container | HTMLElement \| string | - | DOM container for inline mode. Required only when the admin has set position: "inline" on the portal. | | baseUrl | string | Production URL | Override the widget iframe origin. Only useful for sandbox / self-hosted setups. | | debug | boolean | false | Enable debug logging in the browser console. | | email | string | - | Pre-identified visitor email — must be paired with userHash. | | userId | string | - | Host-side stable user identifier — must be paired with userHash. | | userHash | string | - | HMAC-SHA256 of email (or userId), signed with the Agent identity secret. See Identity. |

2. Server-resolved (configured in the Versiq admin portal)

These are not passed by the integration — they are stored in the Agent's widget_config JSONB row and fetched at bootstrap from the API key. To change any of them, edit the Agent in the portal.

| Field | Configured in admin | Notes | | ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- | | theme | Apparence → palette | Full palette (primaryColor, backgroundColor, textColor, borderRadius, fontFamily, colorScheme). | | position | Apparence → position | "bottom-right", "bottom-left", or "inline". | | language | Apparence → langue | ISO 639-1 (e.g. fr, en). Falls back to browser language when unset. | | showProfile | Apparence → panneau profil | Toggles the profile panel in the widget header. | | open | Apparence → état initial | Default open state on page load. Host can still call widget.open() programmatically. | | brand.title | Branding → titre | Custom header title (falls back to a vertical-specific default). | | brand.avatarUrl | Branding → avatar | Custom avatar URL (must originate from the portal upload — arbitrary URLs are rejected). | | vertical | Agent creation | real-estate, b2b-qualification, … Bound to the API key. |

3. Client overrides (advanced — rarely needed in production)

For tooling, A/B previews or staging environments, every server-resolved field above can also be passed as a WidgetConfig argument or data-* attribute. A host-side value, when present, takes precedence over the admin value. This is intentional — but in production you should configure things in the portal so all your sites stay in sync.

// Override only for a staging preview — production should leave this out
createWidget({
  apiKey: "pk_test_...",
  theme: { primaryColor: "#3B82F6" },
  position: "bottom-left",
});

ThemeConfig shape (admin-defined, occasionally overridden)

| Option | Type | Description | | ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- | | primaryColor | string | Primary brand color (hex, e.g., #3B82F6) | | backgroundColor | string | Background color for the widget container | | textColor | string | Text color | | borderRadius | number | Border radius in pixels | | fontFamily | string | Font family | | colorScheme | "light" \| "dark" \| "auto" | Widget color scheme. "auto" follows the visitor's prefers-color-scheme. Defaults to light. |

Motion

The widget animates state changes (open/close resize, message and quick-reply entrances, hover feedback) with a single, coherent motion language: a harmonic duration scale and Material-style easing curves, all transitions kept under 400ms. These motion tokens are internal — they are not part of the ThemeConfig contract and cannot be overridden by integrators (kept internal until a concrete need arises). The widget fully honours prefers-reduced-motion: reduce: when the visitor opts out, every transition and animation is neutralised. Design rationale and the full token table live in ADR-018.

API Reference

createWidget(config)

Creates a new widget instance. The minimal call is createWidget({ apiKey }) — every behaviour comes from the admin portal. The example below illustrates the inline mode where the integrator must additionally supply a container (host-context only).

const widget = createWidget({
  apiKey: "pk_live_your_key",
  // Required only when the admin set position: "inline" on the portal
  container: document.getElementById("widget-container"),
});

VersiqWidget Methods

| Method | Description | | ----------------------------------------------------- | -------------------------------------------------------- | | open() | Open the widget | | close() | Close the widget | | reset() | Reset the conversation | | setTheme(theme: ThemeConfig) | Update theme dynamically | | setColorScheme(scheme: "light" \| "dark" \| "auto") | Sugar over setTheme({ colorScheme }) for dark-mode UIs | | identify(params) | Set host-attested identity (HMAC) | | destroy() | Remove widget and cleanup | | on(event, handler) | Subscribe to events | | off(event, handler) | Unsubscribe from events |

Window API (Script Tag)

When using the script tag, the API is available on window.Versiq:

window.Versiq.open();
window.Versiq.close();
window.Versiq.setTheme({ primaryColor: "#10B981" });

// Wire your own dark-mode toggle to the widget:
window.Versiq.setColorScheme("dark"); // or "light", or "auto"

Events

Subscribe to widget events to react to user interactions.

widget.on("ready", () => {
  console.log("Widget is ready");
});

widget.on("profile-update", () => {
  console.log("Profile was updated"); // signal only, no data
});

widget.on("qualified", () => {
  console.log("Lead qualified"); // signal only, no data
  // Full profile + score: subscribe to the lead.qualified backend webhook
});

widget.on("message", (data) => {
  console.log("New message:", data.message);
});

widget.on("error", (data) => {
  console.error("Widget error:", data.code, data.message);
});

Event Types

| Event | Payload | Description | | --------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | | ready | - | Widget is loaded and ready | | open | - | Widget was opened | | close | - | Widget was closed | | message | { message: WidgetMessage } | New chat message | | profile-update | - | Profile was updated (signal only — subscribe to webhooks for the data) | | qualified | - | Lead qualified (signal only — subscribe to the lead.qualified webhook for the data) | | funnel-stage-change | { stage: string, previousStage: string \| null } | Conversion funnel stage transition | | cta-shown | { ctaType: string, objectiveId?: string } | A call-to-action was displayed | | cta-clicked | { ctaType: string, objectiveId?: string } | A call-to-action was clicked | | lead-captured | { hasEmail: boolean, hasPhone: boolean } | Lead contact details captured | | error | { code: string, message: string } | An error occurred | | quota-warning | { remaining: number, limit: number } | Lead quota running low | | quota-exceeded | - | Lead quota exceeded | | identity-verified | { email: string, userId?: string } | Host identity verified (HMAC) | | widget-unavailable | { reason: "timeout" \| "error", attempts: number } | SDK exhausted all load retries — the widget degrades to a static state |

TypeScript

The package includes full TypeScript support with exported types:

import type {
  WidgetConfig,
  ThemeConfig,
  VersiqWidget,
  WidgetEventType,
  WidgetProfile,
  WidgetMessage,
  B2BProfile, // B2B-specific (backward compat)
} from "@dolard.eu/versiq-widget";

Profile Types

  • WidgetProfile (Record<string, unknown>) — Generic profile used in events. Shape depends on the vertical (BuyerProfile for real-estate, B2BProfile for b2b-qualification).
  • B2BProfile — Typed B2B qualification profile (sector, companySize, etc.). Kept for backward compatibility.

Vertical Resolution

The vertical (real-estate, b2b-qualification, …) is resolved server-side from the API key — you do not pass it from the integration. Each pk_* key is bound to exactly one Agent, and each Agent is bound to one vertical. Switching vertical = creating a new Agent + new key.

Real-Estate flow

When the resolved vertical is real-estate, the widget runs in qualification mode: Versiq qualifies the buyer through conversation and emits the profile — no property data is needed from the integrator (data-dependent tools like searchProperties, getCityStats, estimateProperty are automatically excluded).

// Assumes the real-estate Agent is configured for `inline` mode in
// the admin portal. Only the host-context fields are passed here.
const widget = createWidget({
  apiKey: "pk_live_your_real_estate_key",
  container: "#chat",
});

// UX signals only — trigger a CTA or an analytics event, no lead data here
widget.on("profile-update", () => {
  showProgressIndicator();
});

widget.on("qualified", () => {
  showQualifiedBadge();
});

The profile/score data itself never flows through postMessage (#1993) — it is only delivered server-side via a POST to your configured webhookUrl on lead.qualified, configured in the admin portal:

{
  "event": "lead.qualified",
  "data": {
    "sessionId": "…",
    "profile": { "userType": "prospect", "location": "…", "budget": 450000, "propertyType": "…" }
  }
}

Display Modes

Display mode (bottom-right, bottom-left, inline) is set in the admin portal. From the integrator's side, the only difference is whether you also need to supply a container.

Floating (admin chose bottom-right or bottom-left)

Widget appears as a floating button in the corner of the page. No container needed.

createWidget({ apiKey: "pk_live_your_key" });

Inline (admin chose inline)

Widget is mounted directly into a DOM container you provide.

createWidget({
  apiKey: "pk_live_your_key",
  container: document.getElementById("chat-container"),
});

React Integration

"use client";

import { useEffect, useRef } from "react";
import { createWidget, type VersiqWidget } from "@dolard.eu/versiq-widget";

export function ContactWidget() {
  const widgetRef = useRef<VersiqWidget | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current || widgetRef.current) return;

    // Assumes the Agent is configured for `inline` mode in the admin
    // portal — only the host-context fields (apiKey + container) are passed
    // here. Theme, position, language, etc. come from the server config.
    const widget = createWidget({
      apiKey: "pk_live_your_key",
      container: containerRef.current,
    });

    widgetRef.current = widget;

    return () => {
      widgetRef.current?.destroy();
      widgetRef.current = null;
    };
  }, []);

  return <div ref={containerRef} className="h-[600px]" />;
}

Data Attributes (Script Tag)

All WidgetConfig options can be passed as kebab-cased data-* attributes on the <script> tag. The same scope split as the JavaScript API applies — in practice only data-api-key is needed.

Host-side (essential)

| Attribute | Maps to | Example | | ---------------- | ----------- | --------------------------------------- | | data-api-key | apiKey | data-api-key="pk_live_abc123" | | data-container | container | data-container="#chat" | | data-base-url | baseUrl | data-base-url="https://app.versiq.io" | | data-debug | debug | data-debug="true" | | data-email | email | data-email="[email protected]" | | data-user-id | userId | data-user-id="usr_123" | | data-user-hash | userHash | data-user-hash="<hmac>" |

data-container is a CSS selector and is only needed when the admin set position: "inline" on the portal.

No theme / position / language overrides via data-*. Unlike the programmatic API, server-resolved fields (theme, position, language, showProfile, open) cannot be overridden from the script tag — the legacy data-theme / data-position / data-language / data-show-profile / data-open attributes are silently ignored. Configure these in the admin portal, or use createWidget({ theme, position, … }) for the tooling/preview override path.

Security

The widget runs inside an <iframe> in your visitors' browsers. Three host-side concerns are worth understanding before going to production: identity attestation, the host Permissions-Policy, and your Content-Security-Policy.

Identity verification (HMAC)

By default a visitor is anonymous. If your page already knows who the visitor is (a logged-in user), you can attest that identity to Versiq so leads are attached to the right account — without trusting a client-supplied email blindly.

Pass email (or userId) together with a userHash: an HMAC-SHA256 of the email (or userId), computed server-side with your Agent's identity secret (never expose that secret in the browser). The widget forwards the triple and the backend recomputes the HMAC to verify it; on success it emits the identity-verified event.

// Server-side (your backend) — pseudo-code
const userHash = hmacSHA256(user.email, AGENT_IDENTITY_SECRET);

// Client-side — pass the precomputed hash, never the secret
createWidget({
  apiKey: "pk_live_your_key",
  email: user.email,
  userHash, // injected from your server-rendered page
});

A mismatched or missing userHash leaves the visitor anonymous — the widget keeps working, it just does not attest the identity.

Host page Permissions-Policy

Some widget features rely on browser-level permissions that must be granted by the host page — the iframe itself can't unlock a capability the parent document forbids.

| Feature | Browser permission | Required Permissions-Policy on the host | | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------- | | Autour de moi quick-reply (real-estate) — proposes nearby cities/districts based on the visitor's current coords | geolocation (navigator.geolocation.getCurrentPosition) | geolocation=(self) at minimum (or include the widget origin explicitly) | | Copy property listing / share link buttons | clipboard-write | clipboard-write=(self) (most hosts already inherit the default) |

If your site already ships a Permissions-Policy header (recommended for defense in depth — see MDN), add geolocation=(self) to the list. The widget iframe is created with allow="clipboard-write; geolocation" so the parent's permission propagates automatically once it's not denied.

Minimal header example:

Permissions-Policy: camera=(), microphone=(), geolocation=(self)

What happens if you forget: the widget keeps working — except the geolocation-backed quick-replies log Geolocation has been disabled in this document by permissions policy. to the visitor's console and the chip silently does nothing on click. The underlying conversation still works; the visitor just types the city manually.

Content-Security-Policy

Permissions-Policy controls what the iframe is allowed to do; Content-Security-Policy controls where scripts / iframes / styles can come from. Both are independent and both are needed in production. The CSP directives required for the Versiq widget are documented in the Versiq backend repo at docs/B2B_INTEGRATION.md § "Content-Security-Policy pour intégrateurs".

E2E Test Selectors (data-testid)

For a smoke test that the widget mounted and works on your page, a small core of data-testid selectors is exposed. These are guaranteed not to change without a major version bump.

| Selector | Element | Additional attributes | | ----------------------- | ----------------------------------------------------------------------------- | ---------------------------------- | | widget-root | Chat container (open) | — | | widget-input | Message input field | — | | widget-send | Send button | — | | widget-message | Message bubble | data-role="user" \| "assistant" | | widget-loading-status | Running-tool status label beside the typing dots (e.g. "Recherche de biens…") | — (absent until a tool runs) | | widget-suggestion | Quick reply chip | data-index="<N>" (position) | | widget-cta-button | Call-to-action button | data-objective="<objectiveType>" | | widget-avatar | Header avatar | — |

The widget also exposes a large set of vertical-specific selectors (real-estate map, areas-overview, exploration mode, criteria popup, range slider, contact form). They are internal to the Versiq UI and out of scope for SDK consumers — the full contract lives in the Versiq backend repository under apps/app/src/app/widget/embed/components/.

Example (Playwright)

await page.getByTestId("widget-input").fill("Looking for a 2-bedroom in Lyon");
await page.getByTestId("widget-send").click();
await expect(
  page.getByTestId("widget-message").filter({ hasText: "Lyon" }),
).toBeVisible();

Performance

Bundle Size

| Format | Size (minified) | Size (gzipped) | Limit | | ------ | --------------- | -------------- | ----- | | UMD | ~84 KB | ~24 KB | 50 KB | | ESM | ~112 KB | ~26 KB | - |

CI enforces the 50 KB gzipped limit via size-limit.

Largest Contentful Paint (LCP)

Target: < 1.5s

The widget loads as an iframe, so LCP depends on:

  1. SDK download (~16 KB gzipped) - typically < 100ms
  2. Iframe creation - instant
  3. Embed page load - varies by network

Measured baselines

| Environment | Device | LCP | Notes | | ----------------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------- | | Localhost (no throttle) | Desktop | 0.14s | Reference for development. Not representative of real visitor conditions — production target is 1.5s. | | Marketing demo, Fast 4G | Desktop | to measure | Lighthouse against https://<marketing>/fr/demo/real-estate, throttle "Fast 4G", incognito. | | Marketing demo, Slow 3G | Mobile (mid) | to measure | Same URL, Lighthouse mobile preset, throttle "Slow 3G". | | Marketing demo, Fast 4G | Mobile (mid) | to measure | Same URL, Lighthouse mobile preset, throttle "Fast 4G". |

The three "to measure" rows are reserved for the next manual measurement pass against the marketing demo in prod. They are kept visible here so a visitor of this README sees what is covered and what is not, rather than a single localhost number that paints an over-optimistic picture.

How to measure LCP yourself (Lighthouse)

Reproducible manual procedure on any deployment that embeds the widget (staging, prod demo, or your own integration). The marketing demo is the canonical reference.

  1. Open Chrome in incognito (no extensions, no cache, no service worker from a previous visit interfering with the measurement).
  2. Go to https://<marketing-host>/fr/demo/real-estate (or your own page embedding @dolard.eu/versiq-widget).
  3. Open DevTools → Lighthouse.
  4. Select Performance only, choose the Mode = Navigation.
  5. Pick the device preset (Mobile or Desktop) and the throttling profile (Slow 4G, Fast 4G, or Slow 3G — match the row you want to fill in the table above).
  6. Click Analyze page load.
  7. Read the Largest Contentful Paint metric from the report (top-left block, color-coded green/orange/red against the 2.5s "good" threshold).

Run each scenario 3 times and report the median to absorb cold-cache / network jitter. A single run is not a measurement, it's an anecdote.

Continuous measurement (production sites)

For integrators who want to keep an eye on the metric over time without running Lighthouse manually:

  • Install the Web Vitals Chrome extension for a live LCP overlay while browsing.
  • Wire web-vitals into your analytics pipeline (Google Analytics 4, Sentry, etc.) to track field LCP per real visitor — Lighthouse alone only measures synthetic loads.

The Versiq widget itself does not emit web-vitals events (out of scope — the SDK should not impose an analytics dependency on the host). Integrators own the measurement pipeline for their own page.

Integration Time

Target: < 15 minutes from docs to working widget

The Quick Start section provides copy-paste integration in under 5 minutes.

Browser Support

The SDK ships modern ESM and UMD bundles and targets evergreen browsers:

| Browser | Supported | | ------------------------ | --------------------- | | Chrome / Edge (Chromium) | last 2 major versions | | Firefox | last 2 major versions | | Safari (macOS / iOS) | last 2 major versions | | Internet Explorer | not supported |

The widget relies on <iframe>, postMessage, WeakSet, and fetch — all available in every browser above. For the Node.js toolchain (bundlers, SSR hosts), engines.node is >=18.

Changelog

Release notes are maintained automatically by release-please — see CHANGELOG.md. Versions are bumped lockstep with @dolard.eu/versiq-core-types.

Support

License

Commercial Source-available — see LICENSE.

This is not an open-source release: the source is published so integrators can audit what runs in their visitors' browsers, but fork / redistribution / competing-SDK use are prohibited without a written agreement. Production use against the Versiq backend is governed by the Versiq Commercial Terms of Service.

For commercial licensing inquiries: [email protected]