diff --git a/.changeset/fullpage-logo-icon-and-logo-url.md b/.changeset/fullpage-logo-icon-and-logo-url.md
new file mode 100644
index 0000000..8993700
--- /dev/null
+++ b/.changeset/fullpage-logo-icon-and-logo-url.md
@@ -0,0 +1,17 @@
+---
+'@smooai/chat-widget': minor
+---
+
+Full-page header: square Smooth icon default + customizable `logoUrl`.
+
+**New default icon.** The full-page header avatar previously rendered the full "smooth" wordmark (a wide 550×135 SVG) crammed into the square tile, so it overflowed and looked broken — and it stamped Smoo branding onto customers' pages. It now renders the square Smooth icon (the stylized `th` glyph, `assets/smooth-icon.svg`, 150×150) which sits cleanly `contain`ed and centered in the tile.
+
+**New `logoUrl` config key.** Host pages can now brand the full-page header with their own logo:
+
+```js
+mountFullPageChat({ endpoint: 'wss://ai.smoo.ai/ws', agentId: '…', logoUrl: 'https://cdn.acme.com/logo.svg' });
+// or declaratively:
+//
+```
+
+When set, the header renders `
` sized to `contain` within the tile; otherwise it falls back to the Smooth icon. **Security:** `logoUrl` is validated to absolute `http(s)` only (via the existing `safeHttpUrl` guard) — `javascript:`/`data:`/relative URLs are dropped — and escaped into the `src` attribute, so a hostile config can't inject script.
diff --git a/README.md b/README.md
index 6a76fb7..008ed3f 100644
--- a/README.md
+++ b/README.md
@@ -223,6 +223,7 @@ Declarative attributes mirror the programmatic [`ChatWidgetConfig`](./src/config
| `endpoint` | `endpoint` | **Required.** smooth-operator WebSocket URL. |
| `agent-id` | `agentId` | **Required.** UUID of the agent. |
| `agent-name` | `agentName` | Header label + monogram. Default `Assistant`. |
+| `logo-url` | `logoUrl` | Brand logo in the full-page header tile (`http(s)` only). Falls back to the Smooth icon. |
| `placeholder` | `placeholder` | Composer placeholder. |
| `greeting` | `greeting` | Shown before the first message. |
| `mode` | `mode` | `popover` (default) or `fullpage`. |
diff --git a/assets/smooth-icon.svg b/assets/smooth-icon.svg
new file mode 100644
index 0000000..32ea626
--- /dev/null
+++ b/assets/smooth-icon.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/src/config.test.ts b/src/config.test.ts
index 1a1558e..0c39105 100644
--- a/src/config.test.ts
+++ b/src/config.test.ts
@@ -68,6 +68,15 @@ describe('resolveConfig', () => {
const r = resolveConfig({ ...base, examplePrompts: ['a', ' ', 'b', 'c', 'd', 'e', 'f'] });
expect(r.examplePrompts).toEqual(['a', 'b', 'c', 'd', 'e']);
});
+
+ it('keeps a safe http(s) logoUrl and drops dangerous/relative ones (XSS guard)', () => {
+ expect(resolveConfig({ ...base, logoUrl: 'https://cdn.example.com/l.png' }).logoUrl).toBe('https://cdn.example.com/l.png');
+ // eslint-disable-next-line no-script-url
+ expect(resolveConfig({ ...base, logoUrl: 'javascript:alert(1)' }).logoUrl).toBeUndefined();
+ expect(resolveConfig({ ...base, logoUrl: 'data:text/html,' }).shadowRoot!;
+ expect(sr2.querySelector('.header .logo-img')).toBeNull();
+ });
+
// Configure non-attribute options (examplePrompts / require*) before mount.
function mountCfg(cfg: Record): HTMLElement {
defineChatWidget();
diff --git a/src/element.ts b/src/element.ts
index 020caf2..b434b82 100644
--- a/src/element.ts
+++ b/src/element.ts
@@ -18,7 +18,7 @@ import { AsYouType, isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-
import type { ChatWidgetConfig, ChatWidgetMode, ChatWidgetTheme } from './config.js';
import { needsUserInfo, resolveConfig } from './config.js';
import { type ChatMessage, type Citation, type ConnectionStatus, ConversationController, type IdentityRestore, type Interrupt } from './conversation.js';
-import { SMOOTH_LOGO_SVG } from './logo.js';
+import { SMOOTH_ICON_SVG } from './logo.js';
import { cleanCitationSnippet, escapeHtml, renderMarkdown, safeHttpUrl } from './markdown.js';
import { buildStyles } from './styles.js';
@@ -49,7 +49,7 @@ function phoneToE164(value: string): string | null {
}
}
-const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'placeholder', 'greeting', 'start-open', 'mode'] as const;
+const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'logo-url', 'placeholder', 'greeting', 'start-open', 'mode'] as const;
/**
* Inline SVG icons (static, trusted strings — never interpolated with user data).
@@ -195,6 +195,7 @@ export class SmoothAgentChatElement extends HTMLElement {
mode,
agentId,
agentName: this.overrides.agentName ?? this.getAttribute('agent-name') ?? undefined,
+ logoUrl: this.overrides.logoUrl ?? this.getAttribute('logo-url') ?? undefined,
userName: this.overrides.userName,
userEmail: this.overrides.userEmail,
userPhone: this.overrides.userPhone,
@@ -264,13 +265,19 @@ export class SmoothAgentChatElement extends HTMLElement {
const style = document.createElement('style');
style.textContent = buildStyles(resolved.theme, resolved.mode);
- // Header: in full-page mode lead with the Smooth logo in the avatar tile
+ // Header: in full-page mode lead with the brand logo in the avatar tile
// and a subtle "powered by" tag; in popover mode show a brand-colored
- // monogram avatar + a compact close (collapse) button.
+ // monogram avatar + a compact close (collapse) button. The logo defaults
+ // to the square Smooth icon, but a host page can override it with
+ // `logoUrl` (already sanitized to http(s)-only by resolveConfig; escaped
+ // here so it can't break out of the src attribute).
const monogram = escapeHtml((resolved.agentName.trim().charAt(0) || 'A').toUpperCase());
+ const headerLogo = resolved.logoUrl
+ ? `
`
+ : SMOOTH_ICON_SVG;
const header = fullpage
? `