@helpai/elements 1.8.0 → 1.10.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.
@@ -0,0 +1,570 @@
1
+ # Theming & customization
2
+
3
+ Make the widget match your brand — colors, shapes, typography, and
4
+ launcher style.
5
+
6
+ ---
7
+
8
+ ## TL;DR
9
+
10
+ ```ts
11
+ init({
12
+ publicKey: 'agent_pk_...',
13
+ aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
14
+ theme: {
15
+ accent: '#ff3e00', // your brand primary
16
+ accentText: '#ffffff', // on-accent text
17
+ radius: '12px', // corner rounding
18
+ fontFamily: 'Inter, system-ui, sans-serif',
19
+ },
20
+ launcher: {
21
+ variant: 'pill', // circle / pill / bar / minimal
22
+ label: 'Ask us',
23
+ icon: 'spark', // chat / message / bubbles / spark / wand / bot / help / lifebuoy / headset / search / sparkles / concierge / wave
24
+ },
25
+ composer: {
26
+ sendButton: { shape: 'pill', variant: 'outline' },
27
+ },
28
+ });
29
+ ```
30
+
31
+ Rest of this page goes deep on each knob.
32
+
33
+ ---
34
+
35
+ ## Theme modes
36
+
37
+ Four modes:
38
+
39
+ - **`"auto"`** — follows the visitor's OS preference (`prefers-color-scheme`).
40
+ - **`"light"`** — force light mode.
41
+ - **`"dark"`** — force dark mode.
42
+ - **`"host"`** — follows your page's own light/dark and flips with your site's toggle. The widget reads, in order, a
43
+ `dark` / `light` class on `<html>` or `<body>`, a `data-theme`, `data-color-scheme`, `data-mode` or `data-bs-theme`
44
+ attribute there, then the page's `color-scheme`, then the OS.
45
+
46
+ ```ts
47
+ init({ theme: 'auto' }); // default
48
+ init({ theme: 'light' });
49
+ init({ theme: 'dark' });
50
+ init({ theme: 'host' }); // or <web-ai-chat theme="host">, data-theme="host" on the script tag
51
+ init({ theme: { mode: 'host', accent: '#635146' } }); // a mode beside token overrides
52
+ ```
53
+
54
+ A page that sets only the mode keeps the deployment's token overrides from the dashboard (its brand accent) — so
55
+ `<web-ai-chat theme="host">` follows your site without losing the brand colours.
56
+
57
+ ### Runtime switching
58
+
59
+ End users can flip the theme through the `theme` row in the header's ⋯ menu.
60
+ Their choice persists across conversations and syncs
61
+ across devices.
62
+
63
+ If you don't want a runtime picker, remove the action:
64
+
65
+ ```ts
66
+ init({
67
+ header: {
68
+ actions: ['textSize', 'history', 'close', 'clear', 'expand', 'fullscreen', 'sound', 'language'],
69
+ },
70
+ });
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Color overrides
76
+
77
+ Theme properties you can override:
78
+
79
+ | Option | CSS token | Purpose | Default (help.ai brand) |
80
+ | ----------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
81
+ | `theme.accent` | `--helpai-accent` | Primary brand color | `#10B981` |
82
+ | `theme.accentText` | `--helpai-accent-text` | Text shown on the accent color | `#FFFFFF` |
83
+ | `theme.radius` | `--helpai-radius` | Corner rounding for buttons, cards, pills | `10px` |
84
+ | `theme.fontFamily` | `--helpai-font` | Widget-wide font stack | `Inter, system-ui, -apple-system, Segoe UI, sans-serif` |
85
+ | `theme.contentMaxWidth` | `--helpai-conversation-max` | Max width of the readable content column on a **wide** surface (fullscreen / page / standalone / wide inline) — content centres, bars stay full-bleed. `100%` disables the cap. Modal + drawer fill their card by default. | `768px` |
86
+
87
+ ```ts
88
+ init({
89
+ theme: {
90
+ accent: '#ff3e00',
91
+ accentText: '#fff',
92
+ radius: '16px',
93
+ fontFamily: '"Söhne", -apple-system, sans-serif',
94
+ },
95
+ });
96
+ ```
97
+
98
+ All of these are optional — any you don't set inherits the brand default.
99
+
100
+ ### Where the accent shows up
101
+
102
+ Anywhere the widget draws "chrome" in your brand color:
103
+
104
+ - The launcher FAB background.
105
+ - Send / Stop button (when `composer.sendButton.variant: "filled"`).
106
+ - Active language/theme menu checkmarks.
107
+ - Link colors in rendered markdown.
108
+ - Agent badge status dot.
109
+ - Focus rings.
110
+
111
+ ### Picking an accent
112
+
113
+ Pick any brand color — the widget adapts it per theme so it stays readable, so a
114
+ low-contrast raw color won't render an unreadable widget:
115
+
116
+ - `accent`'s hue + chroma are honored; only its **lightness** is nudged into the
117
+ visible band for the active theme (a near-black accent lightens on dark, a
118
+ near-white one darkens on light).
119
+ - **On-accent text** (the FAB / send glyph) is auto-derived to black or white, so
120
+ `accentText` is **optional** — set it only to force a specific on-fill color
121
+ (it's used as-is only on browsers without CSS relative-color support).
122
+ - **Accent-colored text** (links, active tabs) uses a separate readable shade
123
+ that clears **WCAG AA (4.5:1)** on the panel background for every hue.
124
+
125
+ The one thing to verify yourself is the **FAB against your own page background** —
126
+ it sits on your site, outside the widget's themed surface.
127
+
128
+ Quick contrast check (optional):
129
+
130
+ ```js
131
+ // Paste in DevTools to check contrast
132
+ function contrast(a, b) {
133
+ // WCAG relative luminance formula
134
+ const lum = (hex) => {
135
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
136
+ const lin = (c) => (c < 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
137
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
138
+ };
139
+ const [la, lb] = [lum(a), lum(b)].sort((x, y) => y - x);
140
+ return ((la + 0.05) / (lb + 0.05)).toFixed(2);
141
+ }
142
+ contrast('#ff3e00', '#ffffff'); // 3.79 — OK for large text, fail for small
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Launcher
148
+
149
+ The floating bubble + its nudges. `floating` mode only.
150
+
151
+ ### Variants
152
+
153
+ ```ts
154
+ init({ launcher: { variant: 'circle' } }); // default — icon only, round
155
+ init({ launcher: { variant: 'pill', label: 'Ask AI' } }); // rounded rectangle
156
+ init({ launcher: { variant: 'bar', label: 'Need help?' } }); // wider rectangle
157
+ init({ launcher: { variant: 'minimal', label: 'Chat' } }); // text-only
158
+ ```
159
+
160
+ | Variant | Visual | Best for |
161
+ | --------- | ---------------------- | ------------------------------ |
162
+ | `circle` | 56px round bubble | Classic support chat |
163
+ | `pill` | Icon + label rounded | "Ask AI" marketing CTA |
164
+ | `bar` | Wider prominent bar | High-intent pages |
165
+ | `minimal` | Transparent, text-only | Blends into custom site chrome |
166
+
167
+ ### Sizes
168
+
169
+ ```ts
170
+ init({ launcher: { size: 'sm' } }); // 44px — compact
171
+ init({ launcher: { size: 'md' } }); // 56px — default
172
+ init({ launcher: { size: 'lg' } }); // 68px — prominent
173
+ ```
174
+
175
+ ### Icons
176
+
177
+ ```ts
178
+ init({ launcher: { icon: 'chat' } }); // default — speech bubble
179
+ init({ launcher: { icon: 'message' } }); // message square
180
+ init({ launcher: { icon: 'bubbles' } }); // stacked chat bubbles
181
+ init({ launcher: { icon: 'spark' } }); // sparkle / magic
182
+ init({ launcher: { icon: 'wand' } }); // wand / magic
183
+ init({ launcher: { icon: 'bot' } }); // robot face
184
+ init({ launcher: { icon: 'help' } }); // question mark
185
+ init({ launcher: { icon: 'lifebuoy' } }); // support ring
186
+ init({ launcher: { icon: 'headset' } }); // support agent
187
+ init({ launcher: { icon: 'search' } }); // friendly magnifier (search + smile)
188
+ init({ launcher: { icon: 'sparkles' } }); // multi-point sparkles (AI / magic)
189
+ init({ launcher: { icon: 'concierge' } }); // concierge bell (front-desk)
190
+ init({ launcher: { icon: 'wave' } }); // waving hand (say hi)
191
+ ```
192
+
193
+ Or pass raw SVG source for a fully custom icon:
194
+
195
+ ```ts
196
+ init({
197
+ launcher: {
198
+ icon: `<svg viewBox="0 0 24 24"><path d="M..." /></svg>`,
199
+ },
200
+ });
201
+ ```
202
+
203
+ SVG must be 24×24 viewbox with currentColor strokes/fills.
204
+
205
+ ### Callout
206
+
207
+ A single pre-engagement bubble next to the FAB. One component,
208
+ configured for the use case — greeting, attention nudge, or
209
+ onboarding tip.
210
+
211
+ ```ts
212
+ init({
213
+ launcher: {
214
+ callout: {
215
+ text: "Hey! I'm Aria. What can I help with?",
216
+ shape: 'pill', // pill / bubble / callout
217
+ position: 'auto', // auto / horizontal / vertical
218
+ delayMs: 1200,
219
+ autoDismissMs: 0, // 0 = never (dismiss on click). Or 5000 for auto-dismiss after 5s.
220
+ animated: true, // subtle horizontal nudge animation
221
+ persistent: true, // default — re-show on every visit (greeting behaviour)
222
+ },
223
+ },
224
+ });
225
+ ```
226
+
227
+ #### Dismissal lifecycles
228
+
229
+ `persistent` is the only behavioural difference between the two
230
+ typical patterns:
231
+
232
+ - **Recurring greeting (`persistent: true`, default)** — the bubble
233
+ re-shows on every visit regardless of prior dismissal. Use for
234
+ warm "Hi! 👋" copy that should always greet visitors.
235
+ - **One-time hint (`persistent: false`)** — the exact text is
236
+ persisted to localStorage. Once the user clicks the X (or opens
237
+ the widget once), the same copy stays hidden across visits.
238
+ Reconfiguring with NEW text bypasses the prior dismissal — a new
239
+ message is a new ask.
240
+
241
+ Pick `persistent: false` when the bubble is a CTA you don't want to
242
+ nag with after the first dismissal.
243
+
244
+ Tone: short, conversational. Think "Hi! 👋" or "Click here →" — not
245
+ "CLICK HERE FOR DISCOUNTS".
246
+
247
+ ### Hiding the launcher entirely
248
+
249
+ Want a custom "open chat" button elsewhere on your site?
250
+
251
+ ```ts
252
+ const chat = init({
253
+ publicKey: 'agent_pk_...',
254
+ aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
255
+ launcher: { hidden: true },
256
+ });
257
+
258
+ // Your button
259
+ document.querySelector('#my-chat-btn').onclick = () => chat.open();
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Composer
265
+
266
+ The input area.
267
+
268
+ ### Send button
269
+
270
+ ```ts
271
+ init({
272
+ composer: {
273
+ sendButton: {
274
+ shape: 'pill', // circle / square / pill
275
+ icon: 'arrowUp', // arrowUp / paperPlane / send / chevron
276
+ variant: 'filled', // filled / outline / ghost
277
+ },
278
+ },
279
+ });
280
+ ```
281
+
282
+ 3 × 4 × 3 = 36 combinations. Pick what matches your brand.
283
+
284
+ Quick recipes:
285
+
286
+ - **Minimal outline** — `{ shape: 'circle', variant: 'outline', icon: 'arrowUp' }`
287
+ - **Bold filled** — `{ shape: 'pill', variant: 'filled', icon: 'paperPlane' }`
288
+ - **Ghost text** — `{ shape: 'square', variant: 'ghost', icon: 'send' }`
289
+
290
+ ### Disclaimer line
291
+
292
+ Below the textarea, small grey text. Classic "AI can make mistakes"
293
+ pattern.
294
+
295
+ ```ts
296
+ init({ footer: { composerDisclaimer: 'AI-generated. Verify important details.' } });
297
+ ```
298
+
299
+ Empty string = hidden. Keep it short (< 60 chars for mobile).
300
+
301
+ ### Powered-by row
302
+
303
+ Optional attribution below the disclaimer.
304
+
305
+ ```ts
306
+ init({
307
+ footer: {
308
+ poweredBy: {
309
+ text: 'Powered by Help AI',
310
+ href: 'https://help.ai',
311
+ logoUrl: 'https://cdn.help.ai/logo.svg',
312
+ },
313
+ },
314
+ });
315
+ ```
316
+
317
+ - Omitted fields inherit the active brand's attribution defaults. Set
318
+ `footer.poweredBy.text` to `""` (without a logo) to hide the row.
319
+ - `href` wraps the row in a link with `target="_blank"
320
+ rel="noopener noreferrer"`.
321
+
322
+ ---
323
+
324
+ ## Header actions
325
+
326
+ See [Configuration § Header actions](./configuration.md#header-actions)
327
+ for the full semantics. Quick customization examples:
328
+
329
+ ### Minimal chrome
330
+
331
+ Strip everything but close:
332
+
333
+ ```ts
334
+ init({
335
+ header: { actions: ['close'] },
336
+ });
337
+ ```
338
+
339
+ ### Language-enabled
340
+
341
+ Enable the language row for multilingual marketing. Placement and ordering are
342
+ fixed by the widget; the array is only an enabled set.
343
+
344
+ ```ts
345
+ init({
346
+ i18n: { availableLocales: ['en', 'fr', 'es', 'de'] },
347
+ header: { actions: ['language', 'theme', 'textSize', 'history', 'close', 'clear', 'expand', 'fullscreen', 'sound'] },
348
+ });
349
+ ```
350
+
351
+ ### History-focused chrome
352
+
353
+ Keep the header focused on history and basic preferences:
354
+
355
+ ```ts
356
+ init({
357
+ header: { actions: ['history', 'close', 'clear', 'sound', 'language', 'theme'] },
358
+ features: { disableFileUpload: true, voice: 'disabled' },
359
+ });
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Internationalization
365
+
366
+ ### Language switcher
367
+
368
+ Enable the `language` action — when more than one locale is available, the
369
+ header's ⋯ menu shows one segmented language row:
370
+
371
+ ```ts
372
+ init({
373
+ i18n: {
374
+ availableLocales: ['en', 'fr', 'es', 'de', 'ja'],
375
+ defaultLocale: 'auto', // sniff navigator.language
376
+ },
377
+ header: { actions: ['language', 'theme', 'history', 'close'] },
378
+ });
379
+ ```
380
+
381
+ Locale display names come from `Intl.DisplayNames` in the active
382
+ locale — so a French user sees "anglais, français, espagnol", not
383
+ "English, French, Spanish".
384
+
385
+ The user's pick persists across devices (via the backend).
386
+
387
+ ### Strings overrides
388
+
389
+ Every UI string is configurable. Two shapes:
390
+
391
+ **Flat** (applies to active locale):
392
+
393
+ ```ts
394
+ init({
395
+ i18n: {
396
+ strings: {
397
+ composerPlaceholder: 'Ask me anything…',
398
+ send: 'Go',
399
+ newConversation: 'Start over',
400
+ },
401
+ },
402
+ });
403
+ ```
404
+
405
+ **Nested** (per-locale):
406
+
407
+ ```ts
408
+ init({
409
+ i18n: {
410
+ strings: {
411
+ en: { send: 'Go', composerPlaceholder: 'Ask me anything…' },
412
+ fr: { send: 'Envoyer', composerPlaceholder: 'Demandez-moi…' },
413
+ es: { send: 'Enviar', composerPlaceholder: 'Pregúntame…' },
414
+ },
415
+ },
416
+ });
417
+ ```
418
+
419
+ ### Full list of string keys
420
+
421
+ ```
422
+ attach, attachmentMimeRejected, attachmentTooLarge,
423
+ attachmentTooMany, conversationClosed, close, collapse,
424
+ composerPlaceholder, dateLastWeek, dateOlder, dateToday,
425
+ dateYesterday, dropZone, errorGeneric, errorRetry, expand,
426
+ exitFullscreen, fullscreen, history, historyBack, historyEmpty,
427
+ historyLoading, historyTitle, language, launcherOpen,
428
+ conversationLoading, loadMoreConversations, loadPreviousMessages, micStart, micStop, micUnsupported, moreActions,
429
+ newConversation, panelTitle, resizeHandle, send, soundOff,
430
+ soundOn, startNewConversation, stop, theme, themeAuto, themeDark,
431
+ themeLight, thinking, usedTool
432
+ ```
433
+
434
+ Any key you don't override keeps its default English value.
435
+
436
+ ---
437
+
438
+ ## Dashboard vs client-side
439
+
440
+ **Every theming option above can also be set in your Help AI
441
+ dashboard.** The dashboard lets non-engineers tune colors, copy, and
442
+ launcher chrome without redeploying your site.
443
+
444
+ Precedence:
445
+
446
+ 1. Options you pass to `init()` win.
447
+ 2. Dashboard settings fill in anything you leave unset.
448
+ 3. Built-in defaults are the final fallback.
449
+
450
+ This means you can:
451
+
452
+ - Hardcode brand colors in code (so they never drift even if someone
453
+ edits the dashboard).
454
+ - Let the dashboard own launcher copy (so marketing can A/B-test
455
+ without asking engineering).
456
+ - Mix freely — every option is independently controlled.
457
+
458
+ ---
459
+
460
+ ## CSS-level customization (advanced)
461
+
462
+ The widget renders inside a shadow root, so your page CSS can't reach
463
+ it. If you need to go beyond the `theme` overrides, the widget exposes
464
+ ~40 CSS custom properties on its host element you can tweak directly:
465
+
466
+ ```css
467
+ web-ai-chat,
468
+ helpai-chat-host {
469
+ /* Colors */
470
+ --helpai-bg: #fafafa;
471
+ --helpai-fg: #111;
472
+ --helpai-border: #e5e5e5;
473
+ /* Set the accent via the `-user` slots — the widget DERIVES the theme-adapted
474
+ `--helpai-accent` / `--helpai-accent-text` / `--helpai-accent-fg` from these,
475
+ so overriding the derived tokens directly has no effect on modern browsers. */
476
+ --helpai-accent-user: #ff3e00;
477
+ --helpai-accent-text-user: #fff;
478
+
479
+ /* Panel dimensions */
480
+ --helpai-panel-w: 420px;
481
+ --helpai-panel-h: 640px;
482
+ --helpai-expanded-w: 640px;
483
+ --helpai-expanded-h: 820px;
484
+
485
+ /* Launcher */
486
+ --helpai-fab-size: 56px;
487
+
488
+ /* Typography */
489
+ --helpai-font: "Inter", system-ui, sans-serif;
490
+ --helpai-radius: 10px;
491
+
492
+ /* Motion */
493
+ --helpai-dur-base: 240ms;
494
+ --helpai-dur-quick: 120ms;
495
+ }
496
+ ```
497
+
498
+ These cascade through the shadow root — a single rule on the host
499
+ element propagates to every component inside. Full list in
500
+ `src/styles/tokens.css` in the source.
501
+
502
+ ### Styling parts
503
+
504
+ Five elements carry `part` names, so your own CSS restyles them through the shadow root with `::part()` — no
505
+ reaching into the widget, and names that stay stable across releases:
506
+
507
+ | Part | Element |
508
+ | ---------- | -------------------------------------------------------------------------- |
509
+ | `launcher` | The floating launcher button |
510
+ | `panel` | The open panel (chat, tabbed messenger or empty state) |
511
+ | `header` | The bar at the top of the panel (the chat, history and sub-screen headers) |
512
+ | `messages` | The scrolling message list |
513
+ | `composer` | The message box area |
514
+
515
+ ```css
516
+ web-ai-chat::part(launcher) {
517
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
518
+ }
519
+ web-ai-chat::part(panel) {
520
+ border-radius: 20px;
521
+ }
522
+ web-ai-chat::part(header) {
523
+ border-bottom-color: transparent;
524
+ }
525
+ ```
526
+
527
+ Use `helpai-chat-host::part(…)` for a widget mounted with `init()` / the script tag. Parts don't cross an iframe, so
528
+ `presentation.iframe: true` widgets are styled with tokens only. Prefer the tokens above for colour and size; parts
529
+ are for what tokens don't cover (shadows, corners, borders).
530
+
531
+ For truly bespoke designs (e.g., replacing the launcher with your
532
+ own component), use `launcher.hidden: true` + custom trigger, or a
533
+ [no-script trigger](./api-reference.md#no-script-triggers).
534
+
535
+ ---
536
+
537
+ ## Per-deployment vs per-instance
538
+
539
+ Some knobs are set at the **deployment** level (in your Help AI
540
+ dashboard) and apply to every widget mounted for that deployment:
541
+
542
+ - Theme defaults
543
+ - Launcher chrome defaults
544
+ - Welcome messages + suggestion chips
545
+ - Agent persona (name, avatar, status)
546
+ - Model routing
547
+ - Tool allowlist
548
+ - Action layout defaults
549
+ - I18n strings
550
+
551
+ Other knobs are best set **per-instance** in your `init()` call:
552
+
553
+ - `widgetId` (for multi-widget pages)
554
+ - `target` (for inline/standalone)
555
+ - `user` identity
556
+ - `onMessage` / `onOpen` / `onError` callbacks
557
+
558
+ Any option set in `init()` wins over the deployment default, so you
559
+ can always override.
560
+
561
+ ---
562
+
563
+ ## Next
564
+
565
+ - [Events & hooks](./events-and-hooks.md) — listening to chat
566
+ interactions, integrating with analytics.
567
+ - [Recipes](./recipes.md) — copy-paste examples for common
568
+ customization scenarios.
569
+ - [Configuration reference](./configuration.md) — the full option
570
+ table.