@smooai/chat-widget 0.7.0 → 0.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.
- package/README.md +1 -0
- package/dist/chat-widget.global.js +13407 -1839
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1396 -1269
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/config.ts +15 -1
- package/src/element.ts +131 -9
- package/src/logo.ts +9 -4
- package/src/styles.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smooai/chat-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Embeddable AI chat as a framework-light web component — the Aurora Glass design, streaming replies, grounded sources, and per-brand theming. Speaks the smooth-operator WebSocket protocol.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SmooAI",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@smooai/smooth-operator": "^1.8.0",
|
|
62
|
+
"libphonenumber-js": "^1.13.7",
|
|
62
63
|
"zustand": "^5.0.14"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* `<smooth-agent-chat>` element) or programmatically (passing this object to
|
|
6
6
|
* {@link mountChatWidget} / `element.configure(...)`).
|
|
7
7
|
*/
|
|
8
|
+
import { safeHttpUrl } from './markdown.js';
|
|
9
|
+
|
|
8
10
|
export interface ChatWidgetTheme {
|
|
9
11
|
/** Foreground text color for the widget chrome. */
|
|
10
12
|
text?: string;
|
|
@@ -65,6 +67,13 @@ export interface ChatWidgetConfig {
|
|
|
65
67
|
agentId: string;
|
|
66
68
|
/** Display name for the agent (header label). Defaults to "Assistant". */
|
|
67
69
|
agentName?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Brand logo shown in the full-page header avatar tile; falls back to the
|
|
72
|
+
* Smooth icon. SECURITY: only absolute `http(s)` URLs are honored — any other
|
|
73
|
+
* scheme (`javascript:`/`data:`/…) is ignored, so a hostile config can't
|
|
74
|
+
* inject script.
|
|
75
|
+
*/
|
|
76
|
+
logoUrl?: string;
|
|
68
77
|
/** Optional display name for the user participant. */
|
|
69
78
|
userName?: string;
|
|
70
79
|
/** Optional email address for the user participant. */
|
|
@@ -127,12 +136,14 @@ export interface ChatWidgetConfig {
|
|
|
127
136
|
/** The fully-resolved theme (canonical keys only — aliases are folded in). */
|
|
128
137
|
export type ResolvedTheme = Required<Omit<ChatWidgetTheme, 'chatBubbleInbound' | 'chatBubbleInboundText' | 'chatBubbleOutbound' | 'chatBubbleOutboundText'>>;
|
|
129
138
|
|
|
130
|
-
export type ResolvedConfig = Required<Omit<ChatWidgetConfig, 'theme' | 'userName' | 'userEmail' | 'userPhone' | 'authContext'>> & {
|
|
139
|
+
export type ResolvedConfig = Required<Omit<ChatWidgetConfig, 'theme' | 'userName' | 'userEmail' | 'userPhone' | 'authContext' | 'logoUrl'>> & {
|
|
131
140
|
theme: ResolvedTheme;
|
|
132
141
|
userName?: string;
|
|
133
142
|
userEmail?: string;
|
|
134
143
|
userPhone?: string;
|
|
135
144
|
authContext?: { userId: string; signature: string; timestamp: number };
|
|
145
|
+
/** Sanitized brand logo URL (`http(s)` only) or `undefined` — see {@link ChatWidgetConfig.logoUrl}. */
|
|
146
|
+
logoUrl?: string;
|
|
136
147
|
};
|
|
137
148
|
|
|
138
149
|
/** Resolve a partial config against the built-in defaults. */
|
|
@@ -150,6 +161,9 @@ export function resolveConfig(config: ChatWidgetConfig): ResolvedConfig {
|
|
|
150
161
|
mode: config.mode ?? 'popover',
|
|
151
162
|
agentId: config.agentId,
|
|
152
163
|
agentName: config.agentName ?? 'Assistant',
|
|
164
|
+
// Only absolute http(s) URLs survive — anything else (javascript:/data:/
|
|
165
|
+
// relative) is dropped so the header can never render a hostile logo src.
|
|
166
|
+
logoUrl: safeHttpUrl(config.logoUrl) ?? undefined,
|
|
153
167
|
userName: config.userName,
|
|
154
168
|
userEmail: config.userEmail,
|
|
155
169
|
userPhone: config.userPhone,
|
package/src/element.ts
CHANGED
|
@@ -14,16 +14,42 @@
|
|
|
14
14
|
* <smooth-agent-chat endpoint="ws://localhost:8787/ws" agent-id="…"></smooth-agent-chat>
|
|
15
15
|
* or programmatically via {@link mountChatWidget}.
|
|
16
16
|
*/
|
|
17
|
+
import { AsYouType, isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js/min';
|
|
17
18
|
import type { ChatWidgetConfig, ChatWidgetMode, ChatWidgetTheme } from './config.js';
|
|
18
19
|
import { needsUserInfo, resolveConfig } from './config.js';
|
|
19
20
|
import { type ChatMessage, type Citation, type ConnectionStatus, ConversationController, type IdentityRestore, type Interrupt } from './conversation.js';
|
|
20
|
-
import {
|
|
21
|
+
import { SMOOTH_ICON_SVG } from './logo.js';
|
|
21
22
|
import { cleanCitationSnippet, escapeHtml, renderMarkdown, safeHttpUrl } from './markdown.js';
|
|
22
23
|
import { buildStyles } from './styles.js';
|
|
23
24
|
|
|
24
25
|
export const ELEMENT_TAG = 'smooth-agent-chat';
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Default region for phone parsing/formatting on the pre-chat form. The widget
|
|
29
|
+
* is US-first; the backend does the authoritative E.164 normalization (SMOODEV-2153),
|
|
30
|
+
* so this only governs the as-you-type display + the inline validity hint and the
|
|
31
|
+
* best-effort E.164 we send when the number already parses as valid.
|
|
32
|
+
*/
|
|
33
|
+
const PHONE_DEFAULT_REGION = 'US' as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Best-effort E.164 for an as-typed phone number. Returns the canonical
|
|
37
|
+
* `+1…` form when the value parses to a valid number in {@link PHONE_DEFAULT_REGION},
|
|
38
|
+
* otherwise `null` (caller falls back to sending the raw value — the backend
|
|
39
|
+
* re-parses and normalizes/nulls authoritatively).
|
|
40
|
+
*/
|
|
41
|
+
function phoneToE164(value: string): string | null {
|
|
42
|
+
const v = value.trim();
|
|
43
|
+
if (!v) return null;
|
|
44
|
+
try {
|
|
45
|
+
if (!isValidPhoneNumber(v, PHONE_DEFAULT_REGION)) return null;
|
|
46
|
+
return parsePhoneNumber(v, PHONE_DEFAULT_REGION).number;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'logo-url', 'placeholder', 'greeting', 'start-open', 'mode'] as const;
|
|
27
53
|
|
|
28
54
|
/**
|
|
29
55
|
* Inline SVG icons (static, trusted strings — never interpolated with user data).
|
|
@@ -169,6 +195,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
169
195
|
mode,
|
|
170
196
|
agentId,
|
|
171
197
|
agentName: this.overrides.agentName ?? this.getAttribute('agent-name') ?? undefined,
|
|
198
|
+
logoUrl: this.overrides.logoUrl ?? this.getAttribute('logo-url') ?? undefined,
|
|
172
199
|
userName: this.overrides.userName,
|
|
173
200
|
userEmail: this.overrides.userEmail,
|
|
174
201
|
userPhone: this.overrides.userPhone,
|
|
@@ -238,13 +265,19 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
238
265
|
const style = document.createElement('style');
|
|
239
266
|
style.textContent = buildStyles(resolved.theme, resolved.mode);
|
|
240
267
|
|
|
241
|
-
// Header: in full-page mode lead with the
|
|
268
|
+
// Header: in full-page mode lead with the brand logo in the avatar tile
|
|
242
269
|
// and a subtle "powered by" tag; in popover mode show a brand-colored
|
|
243
|
-
// monogram avatar + a compact close (collapse) button.
|
|
270
|
+
// monogram avatar + a compact close (collapse) button. The logo defaults
|
|
271
|
+
// to the square Smooth icon, but a host page can override it with
|
|
272
|
+
// `logoUrl` (already sanitized to http(s)-only by resolveConfig; escaped
|
|
273
|
+
// here so it can't break out of the src attribute).
|
|
244
274
|
const monogram = escapeHtml((resolved.agentName.trim().charAt(0) || 'A').toUpperCase());
|
|
275
|
+
const headerLogo = resolved.logoUrl
|
|
276
|
+
? `<img src="${escapeHtml(resolved.logoUrl)}" alt="" class="logo-img" />`
|
|
277
|
+
: SMOOTH_ICON_SVG;
|
|
245
278
|
const header = fullpage
|
|
246
279
|
? `<div class="header">
|
|
247
|
-
<div class="avatar"><span class="logo-wrap">${
|
|
280
|
+
<div class="avatar"><span class="logo-wrap">${headerLogo}</span></div>
|
|
248
281
|
<div class="meta">
|
|
249
282
|
<span class="title">${escapeHtml(resolved.agentName)}</span>
|
|
250
283
|
<span class="status"><span class="dot off"></span><span class="status-text"></span></span>
|
|
@@ -270,8 +303,10 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
270
303
|
// Phone is collected by default (optional unless requirePhone). Consent
|
|
271
304
|
// checkboxes default to shown, explicit + unchecked (ADR-048 §a/§3).
|
|
272
305
|
const showPhone = resolved.requirePhone || resolved.collectPhone;
|
|
273
|
-
const field = (name: string, type: string, label: string, autocomplete: string, required: boolean) =>
|
|
274
|
-
`<label class="pc-field"><span>${escapeHtml(label)}</span><input name="${name}" type="${type}" autocomplete="${autocomplete}"${required ? ' required' : ''}
|
|
306
|
+
const field = (name: string, type: string, label: string, autocomplete: string, required: boolean, hint = false) =>
|
|
307
|
+
`<label class="pc-field"><span>${escapeHtml(label)}</span><input name="${name}" type="${type}" autocomplete="${autocomplete}"${required ? ' required' : ''} />${
|
|
308
|
+
hint ? '<span class="pc-hint" aria-live="polite"></span>' : ''
|
|
309
|
+
}</label>`;
|
|
275
310
|
const consentBox = (name: string, label: string) =>
|
|
276
311
|
`<label class="pc-consent"><input name="${name}" type="checkbox" /><span>${escapeHtml(label)}</span></label>`;
|
|
277
312
|
const consentHtml = resolved.collectConsent
|
|
@@ -289,7 +324,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
289
324
|
<form class="pc-form" novalidate>
|
|
290
325
|
${resolved.requireName ? field('name', 'text', 'Name', 'name', true) : ''}
|
|
291
326
|
${resolved.requireEmail ? field('email', 'email', 'Email', 'email', true) : ''}
|
|
292
|
-
${showPhone ? field('phone', 'tel', 'Phone', 'tel', resolved.requirePhone) : ''}
|
|
327
|
+
${showPhone ? field('phone', 'tel', 'Phone', 'tel', resolved.requirePhone, true) : ''}
|
|
293
328
|
${consentHtml}
|
|
294
329
|
<button type="submit" class="pc-submit">Start chat</button>
|
|
295
330
|
</form>
|
|
@@ -351,6 +386,12 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
351
386
|
this.handlePrechatSubmit(pcForm as HTMLFormElement);
|
|
352
387
|
});
|
|
353
388
|
|
|
389
|
+
// Live phone formatting + validity hint (libphonenumber-js, US default).
|
|
390
|
+
// The implicit <label>, type="tel", and autocomplete="tel" from field()
|
|
391
|
+
// are preserved — autofill keeps working — and we also reformat on
|
|
392
|
+
// `change` so a browser-autofilled value gets formatted/validated too.
|
|
393
|
+
this.wirePhoneField(pcForm as HTMLFormElement | null);
|
|
394
|
+
|
|
354
395
|
// Cross-device "Restore my chats": open the panel + start the email entry.
|
|
355
396
|
// AWAIT connect() before showing the email step so a `sessionId` exists by
|
|
356
397
|
// the time the visitor submits — otherwise request-otp could go out with no
|
|
@@ -651,16 +692,97 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
651
692
|
}
|
|
652
693
|
}
|
|
653
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Wire as-you-type formatting + an inline validity hint onto the pre-chat
|
|
697
|
+
* phone input (libphonenumber-js, US default region). Autofill is preserved:
|
|
698
|
+
* the input keeps its `type="tel"` + `autocomplete="tel"` + implicit <label>,
|
|
699
|
+
* and we also reformat on `change` so a browser-autofilled value gets
|
|
700
|
+
* formatted/validated too.
|
|
701
|
+
*
|
|
702
|
+
* As-you-type caret note: `AsYouType` reformats the entire string, which
|
|
703
|
+
* moves the caret to the end on a mid-string edit. To avoid fighting the
|
|
704
|
+
* user, we only rewrite the value when the caret is at the end (the typical
|
|
705
|
+
* append-a-digit case) and never on a deletion — so backspacing the
|
|
706
|
+
* formatting characters works naturally.
|
|
707
|
+
*/
|
|
708
|
+
private wirePhoneField(form: HTMLFormElement | null): void {
|
|
709
|
+
const input = form?.querySelector('input[name="phone"]') as HTMLInputElement | null;
|
|
710
|
+
if (!input) return;
|
|
711
|
+
const hint = input.parentElement?.querySelector('.pc-hint') as HTMLElement | null;
|
|
712
|
+
|
|
713
|
+
const updateHint = () => {
|
|
714
|
+
const v = input.value.trim();
|
|
715
|
+
const field = input.closest('.pc-field');
|
|
716
|
+
if (!v) {
|
|
717
|
+
// Empty is neutral — the field is optional unless requirePhone.
|
|
718
|
+
field?.classList.remove('valid', 'invalid');
|
|
719
|
+
if (hint) hint.textContent = '';
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const ok = isValidPhoneNumber(v, PHONE_DEFAULT_REGION);
|
|
723
|
+
field?.classList.toggle('valid', ok);
|
|
724
|
+
field?.classList.toggle('invalid', !ok);
|
|
725
|
+
if (hint) hint.textContent = ok ? '' : 'Enter a valid phone number';
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const reformat = () => {
|
|
729
|
+
const atEnd = input.selectionStart === input.value.length && input.selectionEnd === input.value.length;
|
|
730
|
+
// Only reformat when appending at the end; never fight a mid-string
|
|
731
|
+
// edit or a deletion (see the caret note above).
|
|
732
|
+
if (atEnd) {
|
|
733
|
+
const formatted = new AsYouType(PHONE_DEFAULT_REGION).input(input.value);
|
|
734
|
+
// Avoid clobbering when the user is deleting: only grow/normalize,
|
|
735
|
+
// not when the formatter would re-add a character they just removed.
|
|
736
|
+
if (formatted.length >= input.value.length) {
|
|
737
|
+
input.value = formatted;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
updateHint();
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
input.addEventListener('input', (ev) => {
|
|
744
|
+
const ie = ev as InputEvent;
|
|
745
|
+
// Don't reformat while deleting — let the user clear characters freely.
|
|
746
|
+
if (typeof ie.inputType === 'string' && ie.inputType.startsWith('delete')) {
|
|
747
|
+
updateHint();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
reformat();
|
|
751
|
+
});
|
|
752
|
+
// Browser autofill / paste-then-blur lands here; format + validate it too.
|
|
753
|
+
input.addEventListener('change', reformat);
|
|
754
|
+
}
|
|
755
|
+
|
|
654
756
|
/** Collect identity + consent from the pre-chat form, then drop into the chat view. */
|
|
655
757
|
private handlePrechatSubmit(form: HTMLFormElement): void {
|
|
656
758
|
if (!form.reportValidity()) return;
|
|
657
759
|
const data = new FormData(form);
|
|
658
760
|
const val = (k: string) => ((data.get(k) as string | null)?.trim() || undefined);
|
|
659
761
|
const checked = (k: string) => data.get(k) === 'on';
|
|
762
|
+
|
|
763
|
+
// Phone: when required, block on an invalid number and surface the hint.
|
|
764
|
+
// When optional, allow submit — the backend normalizes/nulls authoritatively.
|
|
765
|
+
const rawPhone = val('phone');
|
|
766
|
+
const phoneInput = form.querySelector('input[name="phone"]') as HTMLInputElement | null;
|
|
767
|
+
if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
|
|
768
|
+
const required = phoneInput.hasAttribute('required');
|
|
769
|
+
const field = phoneInput.closest('.pc-field');
|
|
770
|
+
field?.classList.add('invalid');
|
|
771
|
+
const hint = field?.querySelector('.pc-hint');
|
|
772
|
+
if (hint) hint.textContent = 'Enter a valid phone number';
|
|
773
|
+
if (required) {
|
|
774
|
+
phoneInput.focus();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Prefer canonical E.164 when it parses; fall back to the raw value
|
|
779
|
+
// otherwise (the backend re-parses + normalizes either way).
|
|
780
|
+
const phone = rawPhone ? (phoneToE164(rawPhone) ?? rawPhone) : undefined;
|
|
781
|
+
|
|
660
782
|
this.controller?.setUserInfo({
|
|
661
783
|
name: val('name'),
|
|
662
784
|
email: val('email'),
|
|
663
|
-
phone
|
|
785
|
+
phone,
|
|
664
786
|
consent: { emailOptIn: checked('emailOptIn'), smsOptIn: checked('smsOptIn') },
|
|
665
787
|
});
|
|
666
788
|
this.userInfoSatisfied = true;
|
package/src/logo.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Smooth logo, inlined as
|
|
3
|
-
*
|
|
2
|
+
* The Smooth logo + icon, inlined as SVG strings so the full-page header can
|
|
3
|
+
* render them without a separate network fetch (the IIFE bundle is
|
|
4
|
+
* self-contained).
|
|
4
5
|
*
|
|
5
|
-
* GENERATED from `assets/smooth-logo.svg` — do not
|
|
6
|
-
*
|
|
6
|
+
* GENERATED from `assets/smooth-logo.svg` / `assets/smooth-icon.svg` — do not
|
|
7
|
+
* edit by hand. Regenerate with:
|
|
8
|
+
* node -e 'const fs=require("fs");process.stdout.write(JSON.stringify(fs.readFileSync("assets/smooth-icon.svg","utf8")))'
|
|
7
9
|
*/
|
|
8
10
|
/* eslint-disable */
|
|
9
11
|
export const SMOOTH_LOGO_SVG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 550 135\">\n <defs>\n <style>\n .cls-1 {\n fill: url(#linear-gradient-3);\n }\n\n .cls-2 {\n fill: url(#linear-gradient-2);\n }\n\n .cls-3 {\n fill: url(#linear-gradient);\n fill-rule: evenodd;\n }\n </style>\n <linearGradient id=\"linear-gradient\" x1=\"115.59\" y1=\"112.81\" x2=\"25.08\" y2=\"22.3\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".3\" stop-color=\"#f49f0a\"/>\n <stop offset=\".79\" stop-color=\"#fb7a4d\"/>\n <stop offset=\"1\" stop-color=\"#ff6b6c\"/>\n </linearGradient>\n <linearGradient id=\"linear-gradient-2\" x1=\"360.91\" y1=\"152.01\" x2=\"202.32\" y2=\"-6.59\" xlink:href=\"#linear-gradient\"/>\n <linearGradient id=\"linear-gradient-3\" x1=\"443.91\" y1=\"30.15\" x2=\"531.36\" y2=\"117.59\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".43\" stop-color=\"#00a6a6\"/>\n <stop offset=\"1\" stop-color=\"#1238dd\"/>\n </linearGradient>\n </defs>\n <path class=\"cls-3\" d=\"M48.28,14.96c-12.39,5.21-22.54,14.64-28.65,26.61-6.12,11.97-7.8,25.72-4.77,38.81,3.04,13.09,10.6,24.69,21.36,32.75,10.76,8.06,24.02,12.05,37.44,11.28,13.42-.77,26.13-6.26,35.9-15.5,9.76-9.24,15.95-21.63,17.46-34.99,1.51-13.36-1.74-26.82-9.19-38.01-1.07-1.61-.64-3.78.97-4.85,1.61-1.07,3.78-.64,4.85.97,8.36,12.56,12.02,27.68,10.32,42.67-1.7,15-8.64,28.91-19.61,39.28-10.96,10.37-25.24,16.54-40.31,17.4-15.07.87-29.96-3.62-42.04-12.66-12.08-9.05-20.58-22.07-23.99-36.77-3.41-14.7-1.51-30.14,5.35-43.58,6.87-13.44,18.26-24.02,32.17-29.87,13.91-5.85,29.44-6.6,43.85-2.11,1.85.57,2.88,2.54,2.3,4.38-.57,1.85-2.54,2.88-4.38,2.3-12.83-4-26.67-3.33-39.06,1.88ZM111.39,19.75c0,2.07-1.68,3.75-3.75,3.75s-3.75-1.68-3.75-3.75,1.68-3.75,3.75-3.75,3.75,1.68,3.75,3.75ZM64.64,59.93c0,1.91,2.39,2.56,7.69,3.88,3.89.97,6.6,2.18,8.15,3.63,1.53,1.45,2.29,3.53,2.29,6.25,0,3.57-1.03,6.26-3.11,8.08-2.07,1.82-5.09,2.73-9.09,2.73h-9.6c-1.97,0-3.57-1.6-3.59-3.57-.01-1.99,1.6-3.61,3.59-3.61h9.41c3.15-.12,4.79-.95,4.91-2.47,0-1.3-1.03-2.21-3.07-2.73-6.91-1.72-11.11-3.44-12.6-5.15-1.48-1.71-2.23-3.77-2.23-6.19,0-6.59,3.2-9.85,9.59-9.8h10.77c1.99,0,3.6,1.61,3.6,3.59s-1.61,3.59-3.6,3.59h-9.69c-1.83,0-3.43.06-3.43,1.77Z\"/>\n <path class=\"cls-2\" d=\"M205.52,48.44h-8.86c-.44-3.75-2.23-6.65-5.38-8.72-3.16-2.07-7.03-3.1-11.6-3.1h0c-3.35,0-6.27.54-8.78,1.62-2.49,1.09-4.44,2.59-5.84,4.48-1.39,1.89-2.08,4.05-2.08,6.46h0c0,2.01.49,3.75,1.46,5.2.97,1.44,2.22,2.63,3.74,3.58,1.53.95,3.13,1.72,4.8,2.32,1.68.6,3.22,1.09,4.62,1.46h0l7.68,2.06c1.97.52,4.17,1.23,6.6,2.14,2.43.92,4.75,2.16,6.98,3.72,2.23,1.56,4.07,3.56,5.52,6,1.45,2.44,2.18,5.43,2.18,8.98h0c0,4.08-1.07,7.77-3.2,11.08-2.12,3.29-5.22,5.91-9.3,7.86-4.08,1.95-9.02,2.92-14.82,2.92h0c-5.43,0-10.11-.87-14.06-2.62-3.95-1.75-7.05-4.19-9.3-7.32-2.25-3.12-3.53-6.75-3.84-10.88h9.46c.25,2.85,1.22,5.21,2.9,7.06,1.69,1.87,3.83,3.25,6.42,4.14,2.6.89,5.41,1.34,8.42,1.34h0c3.49,0,6.63-.57,9.4-1.72,2.79-1.13,4.99-2.73,6.62-4.8,1.63-2.05,2.44-4.46,2.44-7.22h0c0-2.51-.7-4.55-2.1-6.12-1.41-1.57-3.26-2.85-5.54-3.84-2.29-.99-4.77-1.85-7.44-2.58h0l-9.3-2.66c-5.91-1.71-10.59-4.13-14.04-7.28-3.44-3.16-5.16-7.29-5.16-12.38h0c0-4.23,1.15-7.93,3.46-11.1,2.29-3.16,5.39-5.62,9.3-7.38,3.91-1.76,8.27-2.64,13.08-2.64h0c4.88,0,9.21.87,13,2.6,3.8,1.73,6.81,4.11,9.04,7.12,2.23,3,3.4,6.41,3.52,10.22h0ZM229.16,105.18h-8.72v-56.74h8.42v8.86h.74c1.19-3.03,3.1-5.38,5.74-7.06,2.63-1.69,5.79-2.54,9.48-2.54h0c3.75,0,6.87.85,9.36,2.54,2.51,1.68,4.46,4.03,5.86,7.06h.58c1.45-2.92,3.63-5.25,6.54-7,2.91-1.73,6.39-2.6,10.46-2.6h0c5.07,0,9.21,1.58,12.44,4.74,3.23,3.17,4.84,8.09,4.84,14.76h0v37.98h-8.72v-37.98c0-4.19-1.14-7.18-3.42-8.98-2.29-1.79-4.99-2.68-8.1-2.68h0c-3.99,0-7.07,1.2-9.26,3.6-2.2,2.4-3.3,5.43-3.3,9.1h0v36.94h-8.86v-38.86c0-3.23-1.05-5.83-3.14-7.82-2.09-1.97-4.79-2.96-8.08-2.96h0c-2.27,0-4.38.6-6.34,1.8-1.96,1.21-3.53,2.88-4.72,5-1.2,2.13-1.8,4.59-1.8,7.38h0v35.46ZM333.9,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.86-5.85-9.02-10.24-2.15-4.37-3.22-9.49-3.22-15.36h0c0-5.91,1.07-11.07,3.22-15.48,2.16-4.4,5.17-7.82,9.02-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.15,4.41,3.22,9.57,3.22,15.48h0c0,5.87-1.07,10.99-3.22,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM333.9,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.89,0-7.09,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.51,1.99,5.71,2.98,9.6,2.98ZM395.94,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.85-5.85-9-10.24-2.16-4.37-3.24-9.49-3.24-15.36h0c0-5.91,1.08-11.07,3.24-15.48,2.15-4.4,5.15-7.82,9-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.16,4.41,3.24,9.57,3.24,15.48h0c0,5.87-1.08,10.99-3.24,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM395.94,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.88,0-7.08,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.52,1.99,5.72,2.98,9.6,2.98Z\"/>\n <path class=\"cls-1\" d=\"M467.88,48.02v13.28h-35.79v-13.28h35.79ZM439.68,34.38h17.89v53.42c0,1.5.36,2.62,1.08,3.36.72.74,1.88,1.1,3.49,1.1.62,0,1.48-.07,2.59-.21,1.11-.14,1.91-.27,2.38-.41l2.31,13.02c-2.02.58-3.97.97-5.84,1.18-1.88.21-3.66.31-5.33.31-6.08,0-10.7-1.43-13.84-4.28-3.15-2.85-4.72-7.01-4.72-12.48v-55.01ZM506.59,72.63v32.71h-17.89V28.95h17.53v33.53h-1.13c1.4-4.55,3.6-8.21,6.59-11,2.99-2.79,7.01-4.18,12.07-4.18,4,0,7.48.89,10.46,2.67,2.97,1.78,5.28,4.29,6.92,7.54,1.64,3.25,2.46,7.02,2.46,11.33v36.5h-17.89v-33.02c0-3.21-.82-5.73-2.46-7.56-1.64-1.83-3.93-2.74-6.87-2.74-1.92,0-3.62.42-5.1,1.26-1.49.84-2.64,2.04-3.46,3.61-.82,1.57-1.23,3.49-1.23,5.74Z\"/>\n</svg>";
|
|
12
|
+
|
|
13
|
+
/** The square Smooth icon (the stylized `th` glyph) — used as the default full-page header avatar. */
|
|
14
|
+
export const SMOOTH_ICON_SVG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 150 150\">\n <defs>\n <style>\n .cls-1 {\n fill: url(#linear-gradient);\n }\n </style>\n <linearGradient id=\"linear-gradient\" x1=\"31.06\" y1=\"37.6\" x2=\"118.5\" y2=\"125.04\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".43\" stop-color=\"#00a6a6\"/>\n <stop offset=\"1\" stop-color=\"#1238dd\"/>\n </linearGradient>\n </defs>\n <path class=\"cls-1\" d=\"M55.03,55.47v13.28H19.24v-13.28h35.79ZM26.83,41.83h17.89v53.42c0,1.5.36,2.62,1.08,3.36.72.74,1.88,1.1,3.49,1.1.62,0,1.48-.07,2.59-.21,1.11-.14,1.91-.27,2.38-.41l2.31,13.02c-2.02.58-3.97.97-5.84,1.18-1.88.21-3.66.31-5.33.31-6.08,0-10.7-1.43-13.84-4.28-3.15-2.85-4.72-7.01-4.72-12.48v-55.01ZM93.74,80.08v32.71h-17.89V36.39h17.53v33.53h-1.13c1.4-4.55,3.6-8.21,6.59-11,2.99-2.79,7.01-4.18,12.07-4.18,4,0,7.48.89,10.46,2.67,2.97,1.78,5.28,4.29,6.92,7.54,1.64,3.25,2.46,7.02,2.46,11.33v36.5h-17.89v-33.02c0-3.21-.82-5.73-2.46-7.56-1.64-1.83-3.93-2.74-6.87-2.74-1.92,0-3.62.42-5.1,1.26-1.49.84-2.64,2.04-3.46,3.61-.82,1.57-1.23,3.49-1.23,5.74Z\"/>\n</svg>";
|
package/src/styles.ts
CHANGED
|
@@ -176,8 +176,9 @@ export function buildStyles(theme: ResolvedTheme, mode: ChatWidgetMode = 'popove
|
|
|
176
176
|
0 1px 0 rgba(255, 255, 255, .25) inset;
|
|
177
177
|
}
|
|
178
178
|
.avatar svg { width: 22px; height: 22px; }
|
|
179
|
-
.avatar .logo-wrap { display: flex; }
|
|
179
|
+
.avatar .logo-wrap { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
|
|
180
180
|
.avatar .logo { height: 22px; width: auto; display: block; }
|
|
181
|
+
.avatar .logo-img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; display: block; border-radius: 9px; }
|
|
181
182
|
.meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
|
182
183
|
.title { font-weight: 650; font-size: 15.5px; letter-spacing: -.01em; line-height: 1.1; }
|
|
183
184
|
.status {
|
|
@@ -226,6 +227,7 @@ export function buildStyles(theme: ResolvedTheme, mode: ChatWidgetMode = 'popove
|
|
|
226
227
|
.panel.fullpage .header { padding: 18px 22px; }
|
|
227
228
|
.panel.fullpage .avatar { width: 44px; height: 44px; }
|
|
228
229
|
.panel.fullpage .avatar .logo { height: 26px; }
|
|
230
|
+
.panel.fullpage .avatar svg { width: 28px; height: 28px; }
|
|
229
231
|
|
|
230
232
|
/* ────────────────────────────── Messages ──────────────────────────── */
|
|
231
233
|
.messages {
|
|
@@ -515,6 +517,24 @@ export function buildStyles(theme: ResolvedTheme, mode: ChatWidgetMode = 'popove
|
|
|
515
517
|
border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
|
|
516
518
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
|
|
517
519
|
}
|
|
520
|
+
/* Inline phone validity — subtle, themed. Empty stays neutral (optional field). */
|
|
521
|
+
.pc-field.valid input {
|
|
522
|
+
border-color: color-mix(in srgb, var(--sac-primary) 55%, #2faa6a 45%);
|
|
523
|
+
}
|
|
524
|
+
.pc-field.invalid input {
|
|
525
|
+
border-color: color-mix(in srgb, #e2566b 62%, var(--sac-border) 38%);
|
|
526
|
+
}
|
|
527
|
+
.pc-field.invalid input:focus {
|
|
528
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, #e2566b 16%, transparent);
|
|
529
|
+
}
|
|
530
|
+
.pc-field .pc-hint {
|
|
531
|
+
min-height: 13px;
|
|
532
|
+
margin-top: 1px;
|
|
533
|
+
font-size: 11.5px;
|
|
534
|
+
font-weight: 500;
|
|
535
|
+
line-height: 1.2;
|
|
536
|
+
color: color-mix(in srgb, #e2566b 78%, var(--sac-text) 22%);
|
|
537
|
+
}
|
|
518
538
|
.pc-submit {
|
|
519
539
|
margin-top: 4px;
|
|
520
540
|
border: none;
|