@gamaro.io/widget 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,662 @@
1
+ # @gamaro.io/widget
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@gamaro.io/widget.svg)](https://www.npmjs.com/package/@gamaro.io/widget)
4
+ [![bundle size](https://img.shields.io/badge/gzip-%3C50%20KB-brightgreen.svg)](#bundle-size)
5
+ [![types](https://img.shields.io/npm/types/@gamaro.io/widget.svg)](#typescript)
6
+ [![license](https://img.shields.io/badge/license-Commercial%20Source--available-blue.svg)](./LICENSE)
7
+
8
+ Versiq Widget SDK — embed a conversational conversion agent that answers
9
+ visitors using **your** site's data (catalogue, inventory, CRM, knowledge base),
10
+ not the public-web average a generic LLM assistant returns.
11
+
12
+ ## Table of Contents
13
+
14
+ - [Installation](#installation)
15
+ - [Quick Start](#quick-start)
16
+ - [Why this widget converts](#why-this-widget-converts)
17
+ - [Configuration](#configuration)
18
+ - [API Reference](#api-reference)
19
+ - [Events](#events)
20
+ - [TypeScript](#typescript)
21
+ - [Vertical Resolution](#vertical-resolution)
22
+ - [Display Modes](#display-modes)
23
+ - [React Integration](#react-integration)
24
+ - [Data Attributes (Script Tag)](#data-attributes-script-tag)
25
+ - [Security](#security)
26
+ - [E2E Test Selectors (data-testid)](#e2e-test-selectors-data-testid)
27
+ - [Performance](#performance)
28
+ - [Browser Support](#browser-support)
29
+ - [Changelog](#changelog)
30
+ - [Support](#support)
31
+ - [License](#license)
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @gamaro.io/widget
37
+ # or
38
+ pnpm add @gamaro.io/widget
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ### Script Tag (CDN)
44
+
45
+ Hot-link the latest published bundle from any public npm CDN. A single line is
46
+ enough — the publishable key binds the widget to your Agent, everything else is
47
+ resolved server-side.
48
+
49
+ ```html
50
+ <script
51
+ src="https://unpkg.com/@gamaro.io/widget@latest/dist/widget.umd.js"
52
+ data-api-key="pk_your_key"
53
+ ></script>
54
+ ```
55
+
56
+ Equivalent via jsDelivr:
57
+
58
+ ```html
59
+ <script
60
+ src="https://cdn.jsdelivr.net/npm/@gamaro.io/widget@latest/dist/widget.umd.js"
61
+ data-api-key="pk_your_key"
62
+ ></script>
63
+ ```
64
+
65
+ `@latest` always serves the most recent release, so a bug fix or improvement
66
+ reaches your visitors without you editing the snippet. If you need to pin a
67
+ specific version for change-control reasons, replace `@latest` with `@<VERSION>`
68
+ (see [npm](https://www.npmjs.com/package/@gamaro.io/widget) for the list) — at
69
+ the cost of having to bump it manually on every release.
70
+
71
+ ### Programmatic API
72
+
73
+ The minimal integration is one line — every visual and behavioural aspect
74
+ (theme, position, language, open-state, branding) is configured in the **Versiq
75
+ admin portal** for your Agent and resolved server-side from the API key.
76
+
77
+ ```typescript
78
+ import { createWidget } from "@gamaro.io/widget";
79
+
80
+ const widget = createWidget({ apiKey: "pk_live_your_key" });
81
+
82
+ // Control the widget at runtime
83
+ widget.open();
84
+ widget.close();
85
+ widget.reset();
86
+
87
+ // Cleanup (e.g., on SPA route change)
88
+ widget.destroy();
89
+ ```
90
+
91
+ ## Why this widget converts
92
+
93
+ Two reasons most platforms fail to convert mobile visitors, and two reasons this
94
+ widget does:
95
+
96
+ 1. **It speaks with your data, not the public-web average.** The agent answers
97
+ from your catalogue, your stock, your CRM, your knowledge base — what your
98
+ competitors and generic AI assistants don't have.
99
+ 2. **It is driven by your thumb, not your keyboard.** The LLM dynamically picks
100
+ the right component at every turn — quick replies, sliders, product cards —
101
+ instead of forcing a form. Visitors qualify their need in a few taps on
102
+ mobile, where 3+ field forms lose 50%+ of users (HubSpot).
103
+
104
+ The widget rendering layer (`QuickReplies`, `PropertyCard`, `ActionButtons`) is
105
+ the runtime that materialises this. Schemas live in the Versiq backend
106
+ repository and are out of scope for the SDK consumer.
107
+
108
+ ## Configuration
109
+
110
+ Widget configuration is split into three scopes — only the first one is your
111
+ responsibility as an integrator.
112
+
113
+ ### 1. Host-side (passed to `createWidget` or as `data-*` attributes)
114
+
115
+ These can only live on the integrator's page because they describe the host
116
+ context (DOM, identity, environment).
117
+
118
+ | Option | Type | Default | Description |
119
+ | ----------- | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------- |
120
+ | `apiKey` | `string` | **required** | Publishable API key (`pk_live_*`, `pk_test_*`). Binds the widget to one Agent. |
121
+ | `container` | `HTMLElement \| string` | - | DOM container for inline mode. Required only when the admin has set `position: "inline"` on the portal. |
122
+ | `baseUrl` | `string` | Production URL | Override the widget iframe origin. Only useful for sandbox / self-hosted setups. |
123
+ | `debug` | `boolean` | `false` | Enable debug logging in the browser console. |
124
+ | `email` | `string` | - | Pre-identified visitor email — must be paired with `userHash`. |
125
+ | `userId` | `string` | - | Host-side stable user identifier — must be paired with `userHash`. |
126
+ | `userHash` | `string` | - | HMAC-SHA256 of `email` (or `userId`), signed with the Agent identity secret. See _Identity_. |
127
+
128
+ ### 2. Server-resolved (configured in the Versiq admin portal)
129
+
130
+ These are **not** passed by the integration — they are stored in the Agent's
131
+ `widget_config` JSONB row and fetched at bootstrap from the API key. To change
132
+ any of them, edit the Agent in the portal.
133
+
134
+ | Field | Configured in admin | Notes |
135
+ | ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- |
136
+ | `theme` | Apparence → palette | Full palette (`primaryColor`, `backgroundColor`, `textColor`, `borderRadius`, `fontFamily`, `colorScheme`). |
137
+ | `position` | Apparence → position | `"bottom-right"`, `"bottom-left"`, or `"inline"`. |
138
+ | `language` | Apparence → langue | ISO 639-1 (e.g. `fr`, `en`). Falls back to browser language when unset. |
139
+ | `showProfile` | Apparence → panneau profil | Toggles the profile panel in the widget header. |
140
+ | `open` | Apparence → état initial | Default open state on page load. Host can still call `widget.open()` programmatically. |
141
+ | `brand.title` | Branding → titre | Custom header title (falls back to a vertical-specific default). |
142
+ | `brand.avatarUrl` | Branding → avatar | Custom avatar URL (must originate from the portal upload — arbitrary URLs are rejected). |
143
+ | `vertical` | Agent creation | `real-estate`, `b2b-qualification`, … Bound to the API key. |
144
+
145
+ ### 3. Client overrides (advanced — rarely needed in production)
146
+
147
+ For tooling, A/B previews or staging environments, every server-resolved field
148
+ above can also be passed as a `WidgetConfig` argument or `data-*` attribute. A
149
+ host-side value, when present, **takes precedence over the admin value**. This
150
+ is intentional — but in production you should configure things in the portal so
151
+ all your sites stay in sync.
152
+
153
+ ```typescript
154
+ // Override only for a staging preview — production should leave this out
155
+ createWidget({
156
+ apiKey: "pk_test_...",
157
+ theme: { primaryColor: "#3B82F6" },
158
+ position: "bottom-left",
159
+ });
160
+ ```
161
+
162
+ ### `ThemeConfig` shape (admin-defined, occasionally overridden)
163
+
164
+ | Option | Type | Description |
165
+ | ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
166
+ | `primaryColor` | `string` | Primary brand color (hex, e.g., `#3B82F6`) |
167
+ | `backgroundColor` | `string` | Background color for the widget container |
168
+ | `textColor` | `string` | Text color |
169
+ | `borderRadius` | `number` | Border radius in pixels |
170
+ | `fontFamily` | `string` | Font family |
171
+ | `colorScheme` | `"light" \| "dark" \| "auto"` | Widget color scheme. `"auto"` follows the visitor's `prefers-color-scheme`. Defaults to light. |
172
+
173
+ ### Motion
174
+
175
+ The widget animates state changes (open/close resize, message and quick-reply
176
+ entrances, hover feedback) with a single, coherent motion language: a harmonic
177
+ duration scale and Material-style easing curves, all transitions kept under
178
+ 400ms. These **motion tokens are internal** — they are not part of the
179
+ `ThemeConfig` contract and cannot be overridden by integrators (kept internal
180
+ until a concrete need arises). The widget fully honours
181
+ `prefers-reduced-motion: reduce`: when the visitor opts out, every transition
182
+ and animation is neutralised. Design rationale and the full token table live in
183
+ [ADR-018](../../docs/adr/ADR-018-widget-motion-design.md).
184
+
185
+ ## API Reference
186
+
187
+ ### createWidget(config)
188
+
189
+ Creates a new widget instance. The minimal call is `createWidget({ apiKey })` —
190
+ every behaviour comes from the admin portal. The example below illustrates the
191
+ inline mode where the integrator must additionally supply a `container`
192
+ (host-context only).
193
+
194
+ ```typescript
195
+ const widget = createWidget({
196
+ apiKey: "pk_live_your_key",
197
+ // Required only when the admin set position: "inline" on the portal
198
+ container: document.getElementById("widget-container"),
199
+ });
200
+ ```
201
+
202
+ ### VersiqWidget Methods
203
+
204
+ | Method | Description |
205
+ | ----------------------------------------------------- | -------------------------------------------------------- |
206
+ | `open()` | Open the widget |
207
+ | `close()` | Close the widget |
208
+ | `reset()` | Reset the conversation |
209
+ | `setTheme(theme: ThemeConfig)` | Update theme dynamically |
210
+ | `setColorScheme(scheme: "light" \| "dark" \| "auto")` | Sugar over `setTheme({ colorScheme })` for dark-mode UIs |
211
+ | `identify(params)` | Set host-attested identity (HMAC) |
212
+ | `destroy()` | Remove widget and cleanup |
213
+ | `on(event, handler)` | Subscribe to events |
214
+ | `off(event, handler)` | Unsubscribe from events |
215
+
216
+ ### Window API (Script Tag)
217
+
218
+ When using the script tag, the API is available on `window.Versiq`:
219
+
220
+ ```javascript
221
+ window.Versiq.open();
222
+ window.Versiq.close();
223
+ window.Versiq.setTheme({ primaryColor: "#10B981" });
224
+
225
+ // Wire your own dark-mode toggle to the widget:
226
+ window.Versiq.setColorScheme("dark"); // or "light", or "auto"
227
+ ```
228
+
229
+ ## Events
230
+
231
+ Subscribe to widget events to react to user interactions.
232
+
233
+ ```typescript
234
+ widget.on("ready", () => {
235
+ console.log("Widget is ready");
236
+ });
237
+
238
+ widget.on("profile-update", () => {
239
+ console.log("Profile was updated"); // signal only, no data
240
+ });
241
+
242
+ widget.on("qualified", () => {
243
+ console.log("Lead qualified"); // signal only, no data
244
+ // Full profile + score: subscribe to the lead.qualified backend webhook
245
+ });
246
+
247
+ widget.on("message", (data) => {
248
+ console.log("New message:", data.message);
249
+ });
250
+
251
+ widget.on("error", (data) => {
252
+ console.error("Widget error:", data.code, data.message);
253
+ });
254
+ ```
255
+
256
+ ### Event Types
257
+
258
+ | Event | Payload | Description |
259
+ | --------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
260
+ | `ready` | - | Widget is loaded and ready |
261
+ | `open` | - | Widget was opened |
262
+ | `close` | - | Widget was closed |
263
+ | `message` | `{ message: WidgetMessage }` | New chat message |
264
+ | `profile-update` | - | Profile was updated (signal only — subscribe to webhooks for the data) |
265
+ | `qualified` | - | Lead qualified (signal only — subscribe to the `lead.qualified` webhook for the data) |
266
+ | `funnel-stage-change` | `{ stage: string, previousStage: string \| null }` | Conversion funnel stage transition |
267
+ | `cta-shown` | `{ ctaType: string, objectiveId?: string }` | A call-to-action was displayed |
268
+ | `cta-clicked` | `{ ctaType: string, objectiveId?: string }` | A call-to-action was clicked |
269
+ | `lead-captured` | `{ hasEmail: boolean, hasPhone: boolean }` | Lead contact details captured |
270
+ | `error` | `{ code: string, message: string }` | An error occurred |
271
+ | `quota-warning` | `{ remaining: number, limit: number }` | Lead quota running low |
272
+ | `quota-exceeded` | - | Lead quota exceeded |
273
+ | `identity-verified` | `{ email: string, userId?: string }` | Host identity verified (HMAC) |
274
+ | `widget-unavailable` | `{ reason: "timeout" \| "error", attempts: number }` | SDK exhausted all load retries — the widget degrades to a static state |
275
+
276
+ ## TypeScript
277
+
278
+ The package includes full TypeScript support with exported types:
279
+
280
+ ```typescript
281
+ import type {
282
+ WidgetConfig,
283
+ ThemeConfig,
284
+ VersiqWidget,
285
+ WidgetEventType,
286
+ WidgetProfile,
287
+ WidgetMessage,
288
+ B2BProfile, // B2B-specific (backward compat)
289
+ } from "@gamaro.io/widget";
290
+ ```
291
+
292
+ ### Profile Types
293
+
294
+ - **`WidgetProfile`** (`Record<string, unknown>`) — Generic profile used in
295
+ events. Shape depends on the vertical (`BuyerProfile` for real-estate,
296
+ `B2BProfile` for b2b-qualification).
297
+ - **`B2BProfile`** — Typed B2B qualification profile (sector, companySize,
298
+ etc.). Kept for backward compatibility.
299
+
300
+ ## Vertical Resolution
301
+
302
+ The vertical (`real-estate`, `b2b-qualification`, …) is **resolved server-side
303
+ from the API key** — you do not pass it from the integration. Each `pk_*` key is
304
+ bound to exactly one Agent, and each Agent is bound to one vertical. Switching
305
+ vertical = creating a new Agent + new key.
306
+
307
+ ### Real-Estate flow
308
+
309
+ When the resolved vertical is `real-estate`, the widget runs in qualification
310
+ mode: Versiq qualifies the buyer through conversation and emits the profile — no
311
+ property data is needed from the integrator (data-dependent tools like
312
+ `searchProperties`, `getCityStats`, `estimateProperty` are automatically
313
+ excluded).
314
+
315
+ ```typescript
316
+ // Assumes the real-estate Agent is configured for `inline` mode in
317
+ // the admin portal. Only the host-context fields are passed here.
318
+ const widget = createWidget({
319
+ apiKey: "pk_live_your_real_estate_key",
320
+ container: "#chat",
321
+ });
322
+
323
+ // UX signals only — trigger a CTA or an analytics event, no lead data here
324
+ widget.on("profile-update", () => {
325
+ showProgressIndicator();
326
+ });
327
+
328
+ widget.on("qualified", () => {
329
+ showQualifiedBadge();
330
+ });
331
+ ```
332
+
333
+ The profile/score data itself never flows through `postMessage` (#1993) — it is
334
+ only delivered server-side via a POST to your configured `webhookUrl` on
335
+ `lead.qualified`, configured in the admin portal:
336
+
337
+ ```json
338
+ {
339
+ "event": "lead.qualified",
340
+ "data": {
341
+ "sessionId": "…",
342
+ "profile": {
343
+ "userType": "prospect",
344
+ "location": "…",
345
+ "budget": 450000,
346
+ "propertyType": "…"
347
+ }
348
+ }
349
+ }
350
+ ```
351
+
352
+ ## Display Modes
353
+
354
+ Display mode (`bottom-right`, `bottom-left`, `inline`) is set in the admin
355
+ portal. From the integrator's side, the only difference is whether you also need
356
+ to supply a `container`.
357
+
358
+ ### Floating (admin chose `bottom-right` or `bottom-left`)
359
+
360
+ Widget appears as a floating button in the corner of the page. No `container`
361
+ needed.
362
+
363
+ ```typescript
364
+ createWidget({ apiKey: "pk_live_your_key" });
365
+ ```
366
+
367
+ ### Inline (admin chose `inline`)
368
+
369
+ Widget is mounted directly into a DOM container you provide.
370
+
371
+ ```typescript
372
+ createWidget({
373
+ apiKey: "pk_live_your_key",
374
+ container: document.getElementById("chat-container"),
375
+ });
376
+ ```
377
+
378
+ ## React Integration
379
+
380
+ ```tsx
381
+ "use client";
382
+
383
+ import { useEffect, useRef } from "react";
384
+ import { createWidget, type VersiqWidget } from "@gamaro.io/widget";
385
+
386
+ export function ContactWidget() {
387
+ const widgetRef = useRef<VersiqWidget | null>(null);
388
+ const containerRef = useRef<HTMLDivElement>(null);
389
+
390
+ useEffect(() => {
391
+ if (!containerRef.current || widgetRef.current) return;
392
+
393
+ // Assumes the Agent is configured for `inline` mode in the admin
394
+ // portal — only the host-context fields (apiKey + container) are passed
395
+ // here. Theme, position, language, etc. come from the server config.
396
+ const widget = createWidget({
397
+ apiKey: "pk_live_your_key",
398
+ container: containerRef.current,
399
+ });
400
+
401
+ widgetRef.current = widget;
402
+
403
+ return () => {
404
+ widgetRef.current?.destroy();
405
+ widgetRef.current = null;
406
+ };
407
+ }, []);
408
+
409
+ return <div ref={containerRef} className="h-[600px]" />;
410
+ }
411
+ ```
412
+
413
+ ## Data Attributes (Script Tag)
414
+
415
+ All `WidgetConfig` options can be passed as kebab-cased `data-*` attributes on
416
+ the `<script>` tag. The same scope split as the JavaScript API applies — in
417
+ practice only `data-api-key` is needed.
418
+
419
+ ### Host-side (essential)
420
+
421
+ | Attribute | Maps to | Example |
422
+ | ---------------- | ----------- | --------------------------------------- |
423
+ | `data-api-key` | `apiKey` | `data-api-key="pk_live_abc123"` |
424
+ | `data-container` | `container` | `data-container="#chat"` |
425
+ | `data-base-url` | `baseUrl` | `data-base-url="https://gamaro.io/app"` |
426
+ | `data-debug` | `debug` | `data-debug="true"` |
427
+ | `data-email` | `email` | `data-email="user@example.com"` |
428
+ | `data-user-id` | `userId` | `data-user-id="usr_123"` |
429
+ | `data-user-hash` | `userHash` | `data-user-hash="<hmac>"` |
430
+
431
+ `data-container` is a CSS selector and is only needed when the admin set
432
+ `position: "inline"` on the portal.
433
+
434
+ > **No theme / position / language overrides via `data-*`.** Unlike the
435
+ > programmatic API, server-resolved fields (`theme`, `position`, `language`,
436
+ > `showProfile`, `open`) **cannot** be overridden from the script tag — the
437
+ > legacy `data-theme` / `data-position` / `data-language` / `data-show-profile`
438
+ > / `data-open` attributes are silently ignored. Configure these in the admin
439
+ > portal, or use `createWidget({ theme, position, … })` for the tooling/preview
440
+ > override path.
441
+
442
+ ## Security
443
+
444
+ The widget runs inside an `<iframe>` in your visitors' browsers. Three host-side
445
+ concerns are worth understanding before going to production: identity
446
+ attestation, the host `Permissions-Policy`, and your `Content-Security-Policy`.
447
+
448
+ ### Identity verification (HMAC)
449
+
450
+ By default a visitor is anonymous. If your page already knows who the visitor is
451
+ (a logged-in user), you can attest that identity to Versiq so leads are attached
452
+ to the right account — without trusting a client-supplied email blindly.
453
+
454
+ Pass `email` (or `userId`) **together with** a `userHash`: an HMAC-SHA256 of the
455
+ `email` (or `userId`), computed **server-side** with your Agent's identity
456
+ secret (never expose that secret in the browser). The widget forwards the triple
457
+ and the backend recomputes the HMAC to verify it; on success it emits the
458
+ `identity-verified` event.
459
+
460
+ ```typescript
461
+ // Server-side (your backend) — pseudo-code
462
+ const userHash = hmacSHA256(user.email, AGENT_IDENTITY_SECRET);
463
+
464
+ // Client-side — pass the precomputed hash, never the secret
465
+ createWidget({
466
+ apiKey: "pk_live_your_key",
467
+ email: user.email,
468
+ userHash, // injected from your server-rendered page
469
+ });
470
+ ```
471
+
472
+ A mismatched or missing `userHash` leaves the visitor anonymous — the widget
473
+ keeps working, it just does not attest the identity.
474
+
475
+ ### Host page Permissions-Policy
476
+
477
+ Some widget features rely on browser-level permissions that **must be granted by
478
+ the host page** — the iframe itself can't unlock a capability the parent
479
+ document forbids.
480
+
481
+ | Feature | Browser permission | Required `Permissions-Policy` on the host |
482
+ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------- |
483
+ | `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) |
484
+ | Copy property listing / share link buttons | `clipboard-write` | `clipboard-write=(self)` (most hosts already inherit the default) |
485
+
486
+ If your site already ships a `Permissions-Policy` header (recommended for
487
+ defense in depth — see
488
+ [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy)),
489
+ add `geolocation=(self)` to the list. The widget iframe is created with
490
+ `allow="clipboard-write; geolocation"` so the parent's permission propagates
491
+ automatically once it's not denied.
492
+
493
+ Minimal header example:
494
+
495
+ ```http
496
+ Permissions-Policy: camera=(), microphone=(), geolocation=(self)
497
+ ```
498
+
499
+ **What happens if you forget**: the widget keeps working — except the
500
+ geolocation-backed quick-replies log
501
+ `Geolocation has been disabled in this document by permissions policy.` to the
502
+ visitor's console and the chip silently does nothing on click. The underlying
503
+ conversation still works; the visitor just types the city manually.
504
+
505
+ ### Content-Security-Policy
506
+
507
+ Permissions-Policy controls **what the iframe is allowed to do**;
508
+ Content-Security-Policy controls **where scripts / iframes / styles can come
509
+ from**. Both are independent and both are needed in production. The CSP
510
+ directives required for the Versiq widget are documented in the Versiq backend
511
+ repo at `docs/B2B_INTEGRATION.md` § "Content-Security-Policy pour intégrateurs".
512
+
513
+ ## E2E Test Selectors (data-testid)
514
+
515
+ For a smoke test that the widget mounted and works on your page, a small core of
516
+ `data-testid` selectors is exposed. These are guaranteed not to change without a
517
+ major version bump.
518
+
519
+ | Selector | Element | Additional attributes |
520
+ | ----------------------- | ----------------------------------------------------------------------------- | ---------------------------------- |
521
+ | `widget-root` | Chat container (open) | — |
522
+ | `widget-input` | Message input field | — |
523
+ | `widget-send` | Send button | — |
524
+ | `widget-message` | Message bubble | `data-role="user" \| "assistant"` |
525
+ | `widget-loading-status` | Running-tool status label beside the typing dots (e.g. "Recherche de biens…") | — (absent until a tool runs) |
526
+ | `widget-suggestion` | Quick reply chip | `data-index="<N>"` (position) |
527
+ | `widget-cta-button` | Call-to-action button | `data-objective="<objectiveType>"` |
528
+ | `widget-avatar` | Header avatar | — |
529
+
530
+ > The widget also exposes a large set of vertical-specific selectors
531
+ > (real-estate map, areas-overview, exploration mode, criteria popup, range
532
+ > slider, contact form). They are internal to the Versiq UI and out of scope for
533
+ > SDK consumers — the full contract lives in the Versiq backend repository under
534
+ > `apps/app/src/app/widget/embed/components/`.
535
+
536
+ ### Example (Playwright)
537
+
538
+ ```ts
539
+ await page.getByTestId("widget-input").fill("Looking for a 2-bedroom in Lyon");
540
+ await page.getByTestId("widget-send").click();
541
+ await expect(
542
+ page.getByTestId("widget-message").filter({ hasText: "Lyon" }),
543
+ ).toBeVisible();
544
+ ```
545
+
546
+ ## Performance
547
+
548
+ ### Bundle Size
549
+
550
+ | Format | Size (minified) | Size (gzipped) | Limit |
551
+ | ------ | --------------- | -------------- | ----- |
552
+ | UMD | ~84 KB | ~24 KB | 50 KB |
553
+ | ESM | ~112 KB | ~26 KB | - |
554
+
555
+ CI enforces the 50 KB gzipped limit via `size-limit`.
556
+
557
+ ### Largest Contentful Paint (LCP)
558
+
559
+ **Target**: < 1.5s
560
+
561
+ The widget loads as an iframe, so LCP depends on:
562
+
563
+ 1. SDK download (~16 KB gzipped) - typically < 100ms
564
+ 2. Iframe creation - instant
565
+ 3. Embed page load - varies by network
566
+
567
+ #### Measured baselines
568
+
569
+ | Environment | Device | LCP | Notes |
570
+ | ----------------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------- |
571
+ | Localhost (no throttle) | Desktop | 0.14s | Reference for development. Not representative of real visitor conditions — production target is 1.5s. |
572
+ | Marketing demo, Fast 4G | Desktop | _to measure_ | Lighthouse against `https://<marketing>/fr/demo/real-estate`, throttle "Fast 4G", incognito. |
573
+ | Marketing demo, Slow 3G | Mobile (mid) | _to measure_ | Same URL, Lighthouse mobile preset, throttle "Slow 3G". |
574
+ | Marketing demo, Fast 4G | Mobile (mid) | _to measure_ | Same URL, Lighthouse mobile preset, throttle "Fast 4G". |
575
+
576
+ > The three "to measure" rows are reserved for the next manual measurement pass
577
+ > against the marketing demo in prod. They are kept visible here so a visitor of
578
+ > this README sees what is covered and what is not, rather than a single
579
+ > localhost number that paints an over-optimistic picture.
580
+
581
+ #### How to measure LCP yourself (Lighthouse)
582
+
583
+ Reproducible manual procedure on any deployment that embeds the widget (staging,
584
+ prod demo, or your own integration). The marketing demo is the canonical
585
+ reference.
586
+
587
+ 1. Open Chrome in **incognito** (no extensions, no cache, no service worker from
588
+ a previous visit interfering with the measurement).
589
+ 2. Go to `https://<marketing-host>/fr/demo/real-estate` (or your own page
590
+ embedding `@gamaro.io/widget`).
591
+ 3. Open **DevTools → Lighthouse**.
592
+ 4. Select **Performance** only, choose the **Mode = Navigation**.
593
+ 5. Pick the device preset (Mobile or Desktop) and the throttling profile
594
+ (`Slow 4G`, `Fast 4G`, or `Slow 3G` — match the row you want to fill in the
595
+ table above).
596
+ 6. Click **Analyze page load**.
597
+ 7. Read the **Largest Contentful Paint** metric from the report (top-left block,
598
+ color-coded green/orange/red against the 2.5s "good" threshold).
599
+
600
+ Run each scenario **3 times** and report the median to absorb cold-cache /
601
+ network jitter. A single run is not a measurement, it's an anecdote.
602
+
603
+ #### Continuous measurement (production sites)
604
+
605
+ For integrators who want to keep an eye on the metric over time without running
606
+ Lighthouse manually:
607
+
608
+ - Install the
609
+ [**Web Vitals Chrome extension**](https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma)
610
+ for a live LCP overlay while browsing.
611
+ - Wire [**web-vitals**](https://github.com/GoogleChrome/web-vitals) into your
612
+ analytics pipeline (Google Analytics 4, Sentry, etc.) to track field LCP per
613
+ real visitor — Lighthouse alone only measures synthetic loads.
614
+
615
+ The Versiq widget itself does not emit web-vitals events (out of scope — the SDK
616
+ should not impose an analytics dependency on the host). Integrators own the
617
+ measurement pipeline for their own page.
618
+
619
+ ### Integration Time
620
+
621
+ **Target**: < 15 minutes from docs to working widget
622
+
623
+ The Quick Start section provides copy-paste integration in under 5 minutes.
624
+
625
+ ## Browser Support
626
+
627
+ The SDK ships modern ESM and UMD bundles and targets evergreen browsers:
628
+
629
+ | Browser | Supported |
630
+ | ------------------------ | --------------------- |
631
+ | Chrome / Edge (Chromium) | last 2 major versions |
632
+ | Firefox | last 2 major versions |
633
+ | Safari (macOS / iOS) | last 2 major versions |
634
+ | Internet Explorer | not supported |
635
+
636
+ The widget relies on `<iframe>`, `postMessage`, `WeakSet`, and `fetch` — all
637
+ available in every browser above. For the Node.js toolchain (bundlers, SSR
638
+ hosts), `engines.node` is `>=18`.
639
+
640
+ ## Changelog
641
+
642
+ Release notes are maintained automatically by release-please — see
643
+ [CHANGELOG.md](./CHANGELOG.md). Versions are bumped lockstep with
644
+ `@gamaro.io/core-types`.
645
+
646
+ ## Support
647
+
648
+ - **Integration help & bug reports**:
649
+ [open an issue](https://github.com/sdolard/Toize/issues).
650
+ - **Commercial licensing & contracts**: admin@dolard.eu
651
+
652
+ ## License
653
+
654
+ Commercial Source-available — see [LICENSE](./LICENSE).
655
+
656
+ This is **not** an open-source release: the source is published so integrators
657
+ can audit what runs in their visitors' browsers, but fork / redistribution /
658
+ competing-SDK use are prohibited without a written agreement. Production use
659
+ against the Versiq backend is governed by the Versiq Commercial Terms of
660
+ Service.
661
+
662
+ For commercial licensing inquiries: admin@dolard.eu
@@ -0,0 +1,6 @@
1
+ import { type ApplicationConfigResponse } from "./types";
2
+ export declare class ConfigFetchError extends Error {
3
+ readonly code: string;
4
+ constructor(message: string, code: string);
5
+ }
6
+ export declare function fetchApplicationConfig(apiKey: string, baseUrl: string, externalSignal?: AbortSignal): Promise<ApplicationConfigResponse>;