@dolard.eu/versiq-widget 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/README.md +252 -106
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @dolard.eu/versiq-widget
2
2
 
3
- Versiq Widget SDK - Embed conversational qualification into your website.
3
+ Versiq Widget SDK embed a conversational conversion agent that answers
4
+ visitors using **your** site's data (catalogue, inventory, CRM, knowledge base),
5
+ not the public-web average a generic LLM assistant returns.
4
6
 
5
7
  ## Installation
6
8
 
@@ -14,66 +16,148 @@ pnpm add @dolard.eu/versiq-widget
14
16
 
15
17
  ### Script Tag (CDN)
16
18
 
17
- Hot-link from any public npm CDN, pinned to a version:
19
+ Hot-link from any public npm CDN, pinned to a version, with
20
+ [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
21
+ (SRI) so the browser refuses to run a tampered bundle.
22
+
23
+ > **Before copy-pasting**, replace `<VERSION>` with the latest published version
24
+ > (see [npm](https://www.npmjs.com/package/@dolard.eu/versiq-widget)) and
25
+ > `<SRI_HASH>` with the regenerated SHA-384 (see _Regenerating the SRI hash_
26
+ > below). The placeholder values are intentional — every release ships a new
27
+ > bundle, so a hash hardcoded here would always be stale.
18
28
 
19
29
  ```html
20
30
  <script
21
- src="https://unpkg.com/@dolard.eu/versiq-widget@0.1.0/dist/widget.umd.js"
31
+ src="https://unpkg.com/@dolard.eu/versiq-widget@<VERSION>/dist/widget.umd.js"
32
+ integrity="sha384-<SRI_HASH>"
33
+ crossorigin="anonymous"
22
34
  data-api-key="pk_your_key"
23
- data-theme='{"primaryColor":"#3B82F6"}'
24
35
  ></script>
25
36
  ```
26
37
 
27
- Equivalent via jsDelivr:
38
+ Equivalent via jsDelivr (same hash — both CDNs serve the bytes published to npm,
39
+ so the `integrity` value is identical):
28
40
 
29
41
  ```html
30
42
  <script
31
- src="https://cdn.jsdelivr.net/npm/@dolard.eu/versiq-widget@0.1.0/dist/widget.umd.js"
43
+ src="https://cdn.jsdelivr.net/npm/@dolard.eu/versiq-widget@<VERSION>/dist/widget.umd.js"
44
+ integrity="sha384-<SRI_HASH>"
45
+ crossorigin="anonymous"
32
46
  data-api-key="pk_your_key"
33
47
  ></script>
34
48
  ```
35
49
 
36
50
  Pinning a version avoids breaking changes from later releases. To always follow
37
- the latest, drop `@0.1.0`.
51
+ the latest, drop `@<VERSION>` — but then **also drop `integrity`**: SRI locks
52
+ the bundle to one specific publish, you cannot pin "latest hash".
53
+
54
+ #### Regenerating the SRI hash after a version bump
55
+
56
+ Each `@dolard.eu/versiq-widget` release ships a new bundle, so the SRI hash must
57
+ be regenerated and re-pasted into the snippet. From any shell with `curl` and
58
+ `openssl` available:
59
+
60
+ ```bash
61
+ curl -sSL https://unpkg.com/@dolard.eu/versiq-widget@<version>/dist/widget.umd.js \
62
+ | openssl dgst -sha384 -binary | openssl base64 -A
63
+ ```
64
+
65
+ Prefix the output with `sha384-` and copy it as the `integrity` attribute value.
66
+ Verify the result by reloading the host page in a browser — the DevTools Console
67
+ reports a clear error if the hash mismatches.
38
68
 
39
69
  ### Programmatic API
40
70
 
71
+ The minimal integration is one line — every visual and behavioural aspect
72
+ (theme, position, language, open-state, branding) is configured in the **Versiq
73
+ admin portal** for your Application and resolved server-side from the API key.
74
+
41
75
  ```typescript
42
- import { createWidget, type VersiqWidget } from "@dolard.eu/versiq-widget";
76
+ import { createWidget } from "@dolard.eu/versiq-widget";
43
77
 
44
- const widget = createWidget({
45
- apiKey: "pk_your_key",
46
- position: "bottom-right",
47
- theme: { primaryColor: "#3B82F6" },
48
- });
78
+ const widget = createWidget({ apiKey: "pk_live_your_key" });
49
79
 
50
- // Control the widget
80
+ // Control the widget at runtime
51
81
  widget.open();
52
82
  widget.close();
53
83
  widget.reset();
54
84
 
55
- // Cleanup
85
+ // Cleanup (e.g., on SPA route change)
56
86
  widget.destroy();
57
87
  ```
58
88
 
89
+ ## Why this widget converts
90
+
91
+ Two reasons most platforms fail to convert mobile visitors, and two reasons this
92
+ widget does:
93
+
94
+ 1. **It speaks with your data, not the public-web average.** The agent answers
95
+ from your catalogue, your stock, your CRM, your knowledge base — what your
96
+ competitors and generic AI assistants don't have.
97
+ 2. **It is driven by your thumb, not your keyboard.** The LLM dynamically picks
98
+ the right component at every turn — quick replies, sliders, product cards —
99
+ instead of forcing a form. Visitors qualify their need in a few taps on
100
+ mobile, where 3+ field forms lose 50%+ of users (HubSpot).
101
+
102
+ The widget rendering layer (`QuickReplies`, `PropertyCard`, `ActionButtons`) is
103
+ the runtime that materialises this. Schemas live in the Versiq backend
104
+ repository and are out of scope for the SDK consumer.
105
+
59
106
  ## Configuration
60
107
 
61
- ### WidgetConfig
108
+ Widget configuration is split into three scopes — only the first one is your
109
+ responsibility as an integrator.
110
+
111
+ ### 1. Host-side (passed to `createWidget` or as `data-*` attributes)
112
+
113
+ These can only live on the integrator's page because they describe the host
114
+ context (DOM, identity, environment).
115
+
116
+ | Option | Type | Default | Description |
117
+ | ----------- | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------- |
118
+ | `apiKey` | `string` | **required** | Publishable API key (`pk_live_*`, `pk_test_*`). Binds the widget to one Application. |
119
+ | `container` | `HTMLElement \| string` | - | DOM container for inline mode. Required only when the admin has set `position: "inline"` on the portal. |
120
+ | `baseUrl` | `string` | Production URL | Override the widget iframe origin. Only useful for sandbox / self-hosted setups. |
121
+ | `debug` | `boolean` | `false` | Enable debug logging in the browser console. |
122
+ | `email` | `string` | - | Pre-identified visitor email — must be paired with `userHash`. |
123
+ | `userId` | `string` | - | Host-side stable user identifier — must be paired with `userHash`. |
124
+ | `userHash` | `string` | - | HMAC-SHA256 of `email` (or `userId`), signed with the Application identity secret. See _Identity_. |
125
+
126
+ ### 2. Server-resolved (configured in the Versiq admin portal)
127
+
128
+ These are **not** passed by the integration — they are stored in the
129
+ Application's `widget_config` JSONB row and fetched at bootstrap from the API
130
+ key. To change any of them, edit the Application in the portal.
131
+
132
+ | Field | Configured in admin | Notes |
133
+ | ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- |
134
+ | `theme` | Apparence → palette | Full palette (`primaryColor`, `backgroundColor`, `textColor`, `borderRadius`, `fontFamily`, `colorScheme`). |
135
+ | `position` | Apparence → position | `"bottom-right"`, `"bottom-left"`, or `"inline"`. |
136
+ | `language` | Apparence → langue | ISO 639-1 (e.g. `fr`, `en`). Falls back to browser language when unset. |
137
+ | `showProfile` | Apparence → panneau profil | Toggles the profile panel in the widget header. |
138
+ | `open` | Apparence → état initial | Default open state on page load. Host can still call `widget.open()` programmatically. |
139
+ | `brand.title` | Branding → titre | Custom header title (falls back to a vertical-specific default). |
140
+ | `brand.avatarUrl` | Branding → avatar | Custom avatar URL (must originate from the portal upload — arbitrary URLs are rejected). |
141
+ | `vertical` | Application creation | `real-estate`, `b2b-qualification`, … Bound to the API key. |
142
+
143
+ ### 3. Client overrides (advanced — rarely needed in production)
144
+
145
+ For tooling, A/B previews or staging environments, every server-resolved field
146
+ above can also be passed as a `WidgetConfig` argument or `data-*` attribute. A
147
+ host-side value, when present, **takes precedence over the admin value**. This
148
+ is intentional — but in production you should configure things in the portal so
149
+ all your sites stay in sync.
62
150
 
63
- | Option | Type | Default | Description |
64
- | ----------- | --------------------------------------------- | ---------------- | -------------------------------------------- |
65
- | `apiKey` | `string` | **required** | Publishable API key (`pk_*`) for the app |
66
- | `position` | `"bottom-right" \| "bottom-left" \| "inline"` | `"bottom-right"` | Widget position on the page |
67
- | `open` | `boolean` | `false` | Initial open state |
68
- | `container` | `HTMLElement \| string` | - | Container element for inline mode |
69
- | `baseUrl` | `string` | Production URL | Base URL for the widget embed |
70
- | `theme` | `ThemeConfig` | - | Custom theme (merged with server-side theme) |
71
- | `debug` | `boolean` | `false` | Enable debug logging |
72
- | `email` | `string` | - | Pre-identified user email (HMAC) |
73
- | `userId` | `string` | - | Host-side user identifier (HMAC) |
74
- | `userHash` | `string` | - | HMAC-SHA256 of email/userId |
151
+ ```typescript
152
+ // Override only for a staging preview production should leave this out
153
+ createWidget({
154
+ apiKey: "pk_test_...",
155
+ theme: { primaryColor: "#3B82F6" },
156
+ position: "bottom-left",
157
+ });
158
+ ```
75
159
 
76
- ### ThemeConfig
160
+ ### `ThemeConfig` shape (admin-defined, occasionally overridden)
77
161
 
78
162
  | Option | Type | Description |
79
163
  | ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
@@ -88,14 +172,16 @@ widget.destroy();
88
172
 
89
173
  ### createWidget(config)
90
174
 
91
- Creates a new widget instance.
175
+ Creates a new widget instance. The minimal call is `createWidget({ apiKey })` —
176
+ every behaviour comes from the admin portal. The example below illustrates the
177
+ inline mode where the integrator must additionally supply a `container`
178
+ (host-context only).
92
179
 
93
180
  ```typescript
94
181
  const widget = createWidget({
95
- apiKey: "pk_your_key",
96
- position: "inline",
182
+ apiKey: "pk_live_your_key",
183
+ // Required only when the admin set position: "inline" on the portal
97
184
  container: document.getElementById("widget-container"),
98
- open: true,
99
185
  });
100
186
  ```
101
187
 
@@ -191,28 +277,27 @@ import type {
191
277
  - **`B2BProfile`** — Typed B2B qualification profile (sector, companySize,
192
278
  etc.). Kept for backward compatibility.
193
279
 
194
- ## Widget Mode (Real-Estate)
195
-
196
- When using `vertical: "real-estate"`, the widget runs in **qualification mode**:
197
- Versiq qualifies the buyer through conversation and transmits the profile — no
198
- property data is needed from the integrator.
280
+ ## Vertical Resolution
199
281
 
200
- ### Flow
282
+ The vertical (`real-estate`, `b2b-qualification`, …) is **resolved server-side
283
+ from the API key** — you do not pass it from the integration. Each `pk_*` key is
284
+ bound to exactly one Application, and each Application is bound to one vertical.
285
+ Switching vertical = creating a new Application + new key.
201
286
 
202
- 1. Versiq detects the user type (buyer, seller, etc.)
203
- 2. Collects criteria: location, budget, property type, surface...
204
- 3. Emits `profile-update` at each turn with the current profile
205
- 4. When qualification is complete, emits `versiq:qualified` with the full
206
- profile
287
+ ### Real-Estate flow
207
288
 
208
- ### Integration Example
289
+ When the resolved vertical is `real-estate`, the widget runs in qualification
290
+ mode: Versiq qualifies the buyer through conversation and emits the profile — no
291
+ property data is needed from the integrator (data-dependent tools like
292
+ `searchProperties`, `getCityStats`, `estimateProperty` are automatically
293
+ excluded).
209
294
 
210
295
  ```typescript
296
+ // Assumes the real-estate Application is configured for `inline` mode in
297
+ // the admin portal. Only the host-context fields are passed here.
211
298
  const widget = createWidget({
212
- apiKey: "pk_your_real_estate_key",
213
- position: "inline",
299
+ apiKey: "pk_live_your_real_estate_key",
214
300
  container: "#chat",
215
- open: true,
216
301
  });
217
302
 
218
303
  // Track profile updates in real-time
@@ -229,35 +314,29 @@ widget.on("qualified", (data) => {
229
314
  });
230
315
  ```
231
316
 
232
- ### Data Tools
233
-
234
- In widget mode, data-dependent tools (searchProperties, getCityStats,
235
- estimateProperty, etc.) are **automatically excluded**. The integrator's
236
- platform provides the data; Versiq handles qualification only.
237
-
238
317
  ## Display Modes
239
318
 
240
- ### Floating (Default)
319
+ Display mode (`bottom-right`, `bottom-left`, `inline`) is set in the admin
320
+ portal. From the integrator's side, the only difference is whether you also need
321
+ to supply a `container`.
322
+
323
+ ### Floating (admin chose `bottom-right` or `bottom-left`)
241
324
 
242
- Widget appears as a floating button in the corner of the page.
325
+ Widget appears as a floating button in the corner of the page. No `container`
326
+ needed.
243
327
 
244
328
  ```typescript
245
- createWidget({
246
- apiKey: "pk_your_key",
247
- position: "bottom-right", // or "bottom-left"
248
- });
329
+ createWidget({ apiKey: "pk_live_your_key" });
249
330
  ```
250
331
 
251
- ### Inline
332
+ ### Inline (admin chose `inline`)
252
333
 
253
- Widget is embedded directly into a container element.
334
+ Widget is mounted directly into a DOM container you provide.
254
335
 
255
336
  ```typescript
256
337
  createWidget({
257
- apiKey: "pk_your_key",
258
- position: "inline",
338
+ apiKey: "pk_live_your_key",
259
339
  container: document.getElementById("chat-container"),
260
- open: true,
261
340
  });
262
341
  ```
263
342
 
@@ -276,12 +355,12 @@ export function ContactWidget() {
276
355
  useEffect(() => {
277
356
  if (!containerRef.current || widgetRef.current) return;
278
357
 
358
+ // Assumes the Application is configured for `inline` mode in the admin
359
+ // portal — only the host-context fields (apiKey + container) are passed
360
+ // here. Theme, position, language, etc. come from the server config.
279
361
  const widget = createWidget({
280
- apiKey: "pk_your_key",
281
- position: "inline",
362
+ apiKey: "pk_live_your_key",
282
363
  container: containerRef.current,
283
- open: true,
284
- theme: { primaryColor: "#3B82F6" },
285
364
  });
286
365
 
287
366
  widgetRef.current = widget;
@@ -298,36 +377,123 @@ export function ContactWidget() {
298
377
 
299
378
  ## Data Attributes (Script Tag)
300
379
 
380
+ All `WidgetConfig` options can be passed as kebab-cased `data-*` attributes on
381
+ the `<script>` tag. The same scope split as the JavaScript API applies — in
382
+ practice only `data-api-key` is needed.
383
+
384
+ ### Host-side (essential)
385
+
301
386
  | Attribute | Maps to | Example |
302
387
  | ---------------- | ---------- | --------------------------------------- |
303
388
  | `data-api-key` | `apiKey` | `data-api-key="pk_live_abc123"` |
304
- | `data-position` | `position` | `data-position="bottom-left"` |
305
- | `data-open` | `open` | `data-open="true"` |
306
- | `data-theme` | `theme` | `data-theme='{"primaryColor":"#F00"}'` |
307
389
  | `data-base-url` | `baseUrl` | `data-base-url="https://app.versiq.io"` |
308
390
  | `data-debug` | `debug` | `data-debug="true"` |
309
391
  | `data-email` | `email` | `data-email="user@example.com"` |
310
392
  | `data-user-id` | `userId` | `data-user-id="usr_123"` |
311
393
  | `data-user-hash` | `userHash` | `data-user-hash="<hmac>"` |
312
394
 
395
+ ### Overrides (tooling/preview only — use the admin portal in production)
396
+
397
+ | Attribute | Maps to | Example |
398
+ | --------------- | ---------- | -------------------------------------- |
399
+ | `data-position` | `position` | `data-position="bottom-left"` |
400
+ | `data-open` | `open` | `data-open="true"` |
401
+ | `data-theme` | `theme` | `data-theme='{"primaryColor":"#F00"}'` |
402
+
403
+ ## Host Page Permissions Policy
404
+
405
+ Some widget features rely on browser-level permissions that **must be granted by
406
+ the host page** — the iframe itself can't unlock a capability the parent
407
+ document forbids.
408
+
409
+ | Feature | Browser permission | Required `Permissions-Policy` on the host |
410
+ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------- |
411
+ | `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) |
412
+ | Copy property listing / share link buttons | `clipboard-write` | `clipboard-write=(self)` (most hosts already inherit the default) |
413
+
414
+ If your site already ships a `Permissions-Policy` header (recommended for
415
+ defense in depth — see
416
+ [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy)),
417
+ add `geolocation=(self)` to the list. The widget iframe is created with
418
+ `allow="clipboard-write; geolocation"` so the parent's permission propagates
419
+ automatically once it's not denied.
420
+
421
+ ### Minimal example
422
+
423
+ ```http
424
+ Permissions-Policy: camera=(), microphone=(), geolocation=(self)
425
+ ```
426
+
427
+ ### What happens if you forget
428
+
429
+ The widget keeps working — except the geolocation-backed quick-replies log
430
+ `Geolocation has been disabled in this document by permissions policy.` to the
431
+ visitor's console and the chip silently does nothing on click. The underlying
432
+ conversation still works; the visitor just types the city manually.
433
+
313
434
  ## E2E Test Selectors (data-testid)
314
435
 
315
436
  The widget exposes a **stable contract** of `data-testid` attributes for
316
437
  end-to-end testing (Playwright, Cypress, etc.). These selectors are guaranteed
317
438
  not to change without a major version bump.
318
439
 
319
- | Selector | Element | Additional attributes |
320
- | ------------------- | --------------------- | ---------------------------------- |
321
- | `widget-root` | Chat container (open) | — |
322
- | `widget-input` | Message input field | — |
323
- | `widget-send` | Send button | — |
324
- | `widget-message` | Message bubble | `data-role="user" \| "assistant"` |
325
- | `widget-suggestion` | Quick reply chip | `data-index="<N>"` (position) |
326
- | `widget-cta-button` | Call-to-action button | `data-objective="<objectiveType>"` |
327
- | `widget-avatar` | Header avatar | — |
328
-
329
- > Contract source: `apps/app/src/app/widget/embed/components/`. Property card
330
- > selectors (`widget-property-card`) are not yet exposed (pending #727).
440
+ | Selector | Element | Additional attributes |
441
+ | ----------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
442
+ | `widget-root` | Chat container (open) | — |
443
+ | `widget-input` | Message input field | — |
444
+ | `widget-send` | Send button | — |
445
+ | `widget-message` | Message bubble | `data-role="user" \| "assistant"` |
446
+ | `widget-suggestion` | Quick reply chip | `data-index="<N>"` (position) |
447
+ | `widget-cta-button` | Call-to-action button | `data-objective="<objectiveType>"` |
448
+ | `widget-avatar` | Header avatar | — |
449
+ | `widget-map-toggle` | Map open/reopen control (FAB mobile / button desktop) | — _(real-estate vertical, #1164)_ |
450
+ | `widget-map-panel` | Map container — desktop `<aside>` or mobile `<Sheet>` | — _(real-estate vertical, #1164)_ |
451
+ | `widget-map-close` | Map close button (desktop only) | — _(real-estate vertical, #1164)_ |
452
+ | `widget-areas-overview` | Areas-overview fullscreen panel root | — _(real-estate vertical, #1165)_ |
453
+ | `widget-areas-overview-close` | Close button (top-right) on the panel | — _(real-estate vertical, #1165)_ |
454
+ | `widget-areas-overview-submit` | Validate-selection button (footer) | — _(real-estate vertical, #1165)_ |
455
+ | `widget-areas-overview-empty` | Empty-state message when `areas.length === 0` | — _(real-estate vertical, #1165)_ |
456
+ | `widget-area-card-<slug>` | Single area card | `data-area="<area>"`, `data-state="kept\|rejected\|toExplore"` _(#1165)_ |
457
+ | `widget-area-keep-<slug>` | Favori toggle button on a card | `data-area="<area>"` _(#1165)_ |
458
+ | `widget-area-reject-<slug>` | Rejeter toggle button on a card | `data-area="<area>"` _(#1165)_ |
459
+ | `widget-exploration-mode` | Exploration overlay root (single-city deep-dive) | `data-city="<city>"` _(real-estate vertical, #1166)_ |
460
+ | `widget-exploration-mode-close` | Close button (×) on the overlay | — _(#1166)_ |
461
+ | `widget-exploration-mode-modify-criteria` | Footer button opening `EmbedCriteriaPopup` | — _(#1166)_ |
462
+ | `widget-exploration-mode-exit` | Footer "Terminer" button (immediate exit, sends current state) | — _(#1166)_ |
463
+ | `widget-exploration-mode-empty` | Empty-state when `initialProperties === []` | — _(#1166)_ |
464
+ | `widget-exploration-mode-empty-modify-criteria` | Modify-criteria CTA inside the empty state | — _(#1166)_ |
465
+ | `widget-exploration-mode-recap` | Recap screen shown once every property has been classified | — _(#1166)_ |
466
+ | `widget-exploration-mode-recap-finish` | Send-selection CTA on the recap screen | — _(#1166)_ |
467
+ | `widget-exploration-property-<id>` | Currently displayed property card | `data-property-id="<id>"`, `data-state="favorite\|rejected\|neutral"` _(#1166)_ |
468
+ | `widget-exploration-favorite-<id>` | Favori toggle button for the current property | `data-property-id="<id>"` _(#1166)_ |
469
+ | `widget-exploration-reject-<id>` | Écarter toggle button for the current property | `data-property-id="<id>"` _(#1166)_ |
470
+ | `widget-exploration-previous` | Navigate to the previous property | — _(#1166)_ |
471
+ | `widget-exploration-next` | Navigate to the next property | — _(#1166)_ |
472
+ | `widget-criteria-popup` | Criteria edit modal root (rendered inside ExplorationMode) | — _(#1166)_ |
473
+ | `widget-criteria-popup-close` | Close button (×) of the criteria modal | — _(#1166)_ |
474
+ | `widget-criteria-popup-cancel` | Footer "Annuler" button | — _(#1166)_ |
475
+ | `widget-criteria-popup-apply` | Footer "Appliquer" button | — _(#1166)_ |
476
+ | `widget-criteria-popup-warning` | Warning banner shown when at least one field changed | — _(#1166)_ |
477
+ | `widget-criteria-budget-min` | Budget min input | — _(#1166)_ |
478
+ | `widget-criteria-budget-max` | Budget max input | — _(#1166)_ |
479
+ | `widget-criteria-surface-min` | Surface min input | — _(#1166)_ |
480
+ | `widget-criteria-surface-max` | Surface max input | — _(#1166)_ |
481
+ | `widget-criteria-rooms` | Rooms min input | — _(#1166)_ |
482
+ | `widget-criteria-property-type-<type>` | Property-type toggle (`apartment`, `house`, `all`) | `data-selected="true\|false"` _(#1166)_ |
483
+
484
+ > Contract source lives in the Versiq backend repository
485
+ > ([sdolard/Toize](https://github.com/sdolard/Toize), path
486
+ > `apps/app/src/app/widget/embed/components/`). Property card selectors
487
+ > (`widget-property-card`) are not yet exposed (pending #727). Map selectors
488
+ > only appear when `MapContext.isVisible === true` — i.e. once the LLM has
489
+ > profiled a geocodable city. On B2B verticals without a map, they never render.
490
+ > Areas-overview selectors only appear when the LLM has streamed an
491
+ > `<!--AREAS_OVERVIEW:-->` marker; `<slug>` is the city name lowercased, accent-
492
+ > stripped (NFD), with non-alphanumeric runs collapsed to `-` (e.g. `Vénissieux`
493
+ > → `venissieux`). Exploration-mode and criteria-popup selectors
494
+ > (`widget-exploration-*` / `widget-criteria-*`) only appear after the
495
+ > `enterExplorationMode` tool has streamed an `<!--EXPLORATION_MODE:-->` marker
496
+ > (real-estate vertical, #1166).
331
497
 
332
498
  ### Example (Playwright)
333
499
 
@@ -339,34 +505,14 @@ await expect(
339
505
  ).toBeVisible();
340
506
  ```
341
507
 
342
- ## Development
343
-
344
- ```bash
345
- # Install dependencies
346
- pnpm install
347
-
348
- # Development build with watch
349
- pnpm dev
350
-
351
- # Production build
352
- pnpm build
353
-
354
- # Run tests
355
- pnpm test
356
-
357
- # Local testing (serves test.html via HTTP - required for iframe CSP)
358
- pnpm serve
359
- # Then open http://localhost:5500/test.html
360
- ```
361
-
362
508
  ## Performance
363
509
 
364
510
  ### Bundle Size
365
511
 
366
512
  | Format | Size (minified) | Size (gzipped) | Limit |
367
513
  | ------ | --------------- | -------------- | ----- |
368
- | UMD | ~63 KB | ~16 KB | 50 KB |
369
- | ESM | ~89 KB | ~19 KB | - |
514
+ | UMD | ~84 KB | ~24 KB | 50 KB |
515
+ | ESM | ~112 KB | ~26 KB | - |
370
516
 
371
517
  CI enforces the 50 KB gzipped limit via `size-limit`.
372
518
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolard.eu/versiq-widget",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Versiq Widget SDK - Embed conversational qualification into your website.",
5
5
  "type": "module",
6
6
  "main": "./dist/widget.umd.js",
@@ -56,7 +56,7 @@
56
56
  ],
57
57
  "dependencies": {
58
58
  "zod": "^4.2.0",
59
- "@dolard.eu/versiq-core-types": "0.1.0"
59
+ "@dolard.eu/versiq-core-types": "0.2.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@size-limit/file": "^11.0.0",