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.2.0

Published

Versiq Widget SDK - Embed conversational qualification into your website.

Downloads

257

Readme

@dolard.eu/versiq-widget

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.

Installation

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

Quick Start

Script Tag (CDN)

Hot-link from any public npm CDN, pinned to a version, with Subresource Integrity (SRI) so the browser refuses to run a tampered bundle.

Before copy-pasting, replace <VERSION> with the latest published version (see npm) and <SRI_HASH> with the regenerated SHA-384 (see Regenerating the SRI hash below). The placeholder values are intentional — every release ships a new bundle, so a hash hardcoded here would always be stale.

<script
  src="https://unpkg.com/@dolard.eu/versiq-widget@<VERSION>/dist/widget.umd.js"
  integrity="sha384-<SRI_HASH>"
  crossorigin="anonymous"
  data-api-key="pk_your_key"
></script>

Equivalent via jsDelivr (same hash — both CDNs serve the bytes published to npm, so the integrity value is identical):

<script
  src="https://cdn.jsdelivr.net/npm/@dolard.eu/versiq-widget@<VERSION>/dist/widget.umd.js"
  integrity="sha384-<SRI_HASH>"
  crossorigin="anonymous"
  data-api-key="pk_your_key"
></script>

Pinning a version avoids breaking changes from later releases. To always follow the latest, drop @<VERSION> — but then also drop integrity: SRI locks the bundle to one specific publish, you cannot pin "latest hash".

Regenerating the SRI hash after a version bump

Each @dolard.eu/versiq-widget release ships a new bundle, so the SRI hash must be regenerated and re-pasted into the snippet. From any shell with curl and openssl available:

curl -sSL https://unpkg.com/@dolard.eu/versiq-widget@<version>/dist/widget.umd.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

Prefix the output with sha384- and copy it as the integrity attribute value. Verify the result by reloading the host page in a browser — the DevTools Console reports a clear error if the hash mismatches.

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 Application 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 Application. | | 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 Application 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 Application's widget_config JSONB row and fetched at bootstrap from the API key. To change any of them, edit the Application 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 | Application 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. |

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", (data) => {
  console.log("Profile updated:", data.profile);
});

widget.on("qualified", (data) => {
  console.log("Lead qualified:", data.profile, "Score:", data.score);
});

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: WidgetProfile } | Profile data was updated | | qualified | { profile: WidgetProfile, score: number } | Lead qualification complete | | 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) |

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 Application, and each Application is bound to one vertical. Switching vertical = creating a new Application + 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 Application 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",
});

// Track profile updates in real-time
widget.on("profile-update", (data) => {
  console.log("Criteria so far:", data.profile);
  // data.profile: { userType, location, budget, propertyType, ... }
});

// Lead is fully qualified — trigger your own search/CRM
widget.on("qualified", (data) => {
  console.log("Qualified lead:", data.profile);
  // data.score: 1 (V1: binary — 1 = qualified)
  triggerPropertySearch(data.profile);
});

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 Application 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-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>" |

Overrides (tooling/preview only — use the admin portal in production)

| Attribute | Maps to | Example | | --------------- | ---------- | -------------------------------------- | | data-position | position | data-position="bottom-left" | | data-open | open | data-open="true" | | data-theme | theme | data-theme='{"primaryColor":"#F00"}' |

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 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.

E2E Test Selectors (data-testid)

The widget exposes a stable contract of data-testid attributes for end-to-end testing (Playwright, Cypress, etc.). These selectors 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-suggestion | Quick reply chip | data-index="<N>" (position) | | widget-cta-button | Call-to-action button | data-objective="<objectiveType>" | | widget-avatar | Header avatar | — | | widget-map-toggle | Map open/reopen control (FAB mobile / button desktop) | — (real-estate vertical, #1164) | | widget-map-panel | Map container — desktop <aside> or mobile <Sheet> | — (real-estate vertical, #1164) | | widget-map-close | Map close button (desktop only) | — (real-estate vertical, #1164) | | widget-areas-overview | Areas-overview fullscreen panel root | — (real-estate vertical, #1165) | | widget-areas-overview-close | Close button (top-right) on the panel | — (real-estate vertical, #1165) | | widget-areas-overview-submit | Validate-selection button (footer) | — (real-estate vertical, #1165) | | widget-areas-overview-empty | Empty-state message when areas.length === 0 | — (real-estate vertical, #1165) | | widget-area-card-<slug> | Single area card | data-area="<area>", data-state="kept\|rejected\|toExplore" (#1165) | | widget-area-keep-<slug> | Favori toggle button on a card | data-area="<area>" (#1165) | | widget-area-reject-<slug> | Rejeter toggle button on a card | data-area="<area>" (#1165) | | widget-exploration-mode | Exploration overlay root (single-city deep-dive) | data-city="<city>" (real-estate vertical, #1166) | | widget-exploration-mode-close | Close button (×) on the overlay | — (#1166) | | widget-exploration-mode-modify-criteria | Footer button opening EmbedCriteriaPopup | — (#1166) | | widget-exploration-mode-exit | Footer "Terminer" button (immediate exit, sends current state) | — (#1166) | | widget-exploration-mode-empty | Empty-state when initialProperties === [] | — (#1166) | | widget-exploration-mode-empty-modify-criteria | Modify-criteria CTA inside the empty state | — (#1166) | | widget-exploration-mode-recap | Recap screen shown once every property has been classified | — (#1166) | | widget-exploration-mode-recap-finish | Send-selection CTA on the recap screen | — (#1166) | | widget-exploration-property-<id> | Currently displayed property card | data-property-id="<id>", data-state="favorite\|rejected\|neutral" (#1166) | | widget-exploration-favorite-<id> | Favori toggle button for the current property | data-property-id="<id>" (#1166) | | widget-exploration-reject-<id> | Écarter toggle button for the current property | data-property-id="<id>" (#1166) | | widget-exploration-previous | Navigate to the previous property | — (#1166) | | widget-exploration-next | Navigate to the next property | — (#1166) | | widget-criteria-popup | Criteria edit modal root (rendered inside ExplorationMode) | — (#1166) | | widget-criteria-popup-close | Close button (×) of the criteria modal | — (#1166) | | widget-criteria-popup-cancel | Footer "Annuler" button | — (#1166) | | widget-criteria-popup-apply | Footer "Appliquer" button | — (#1166) | | widget-criteria-popup-warning | Warning banner shown when at least one field changed | — (#1166) | | widget-criteria-budget-min | Budget min input | — (#1166) | | widget-criteria-budget-max | Budget max input | — (#1166) | | widget-criteria-surface-min | Surface min input | — (#1166) | | widget-criteria-surface-max | Surface max input | — (#1166) | | widget-criteria-rooms | Rooms min input | — (#1166) | | widget-criteria-property-type-<type> | Property-type toggle (apartment, house, all) | data-selected="true\|false" (#1166) |

Contract source lives in the Versiq backend repository (sdolard/Toize, path apps/app/src/app/widget/embed/components/). Property card selectors (widget-property-card) are not yet exposed (pending #727). Map selectors only appear when MapContext.isVisible === true — i.e. once the LLM has profiled a geocodable city. On B2B verticals without a map, they never render. Areas-overview selectors only appear when the LLM has streamed an <!--AREAS_OVERVIEW:--> marker; <slug> is the city name lowercased, accent- stripped (NFD), with non-alphanumeric runs collapsed to - (e.g. Vénissieuxvenissieux). Exploration-mode and criteria-popup selectors (widget-exploration-* / widget-criteria-*) only appear after the enterExplorationMode tool has streamed an <!--EXPLORATION_MODE:--> marker (real-estate vertical, #1166).

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 baseline: 0.14s (localhost, no throttling)

Integration Time

Target: < 15 minutes from docs to working widget

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

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]