@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smooai/chat-widget",
3
- "version": "0.7.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 { SMOOTH_LOGO_SVG } from './logo.js';
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
- const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'placeholder', 'greeting', 'start-open', 'mode'] as const;
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 Smooth logo in the avatar tile
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">${SMOOTH_LOGO_SVG}</span></div>
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' : ''} /></label>`;
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: val('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 an SVG string so the full-page header can render
3
- * it without a separate network fetch (the IIFE bundle is self-contained).
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 edit by hand. Regenerate with:
6
- * node -e ... (see the commit that added this file)
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;