@clivly/chat-widget 0.3.0 → 0.5.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/dist/index.d.mts CHANGED
@@ -1,6 +1,35 @@
1
1
  import { Root } from "react-dom/client";
2
- import { ReactElement } from "react";
2
+ import { CSSProperties, ReactElement } from "react";
3
3
 
4
+ //#region src/theme.d.ts
5
+ /**
6
+ * Theme resolution for the widget.
7
+ *
8
+ * The script-tag build mounts into a Shadow DOM, so the host's `.dark` class on
9
+ * `<html>` can never reach the widget through a CSS selector. The theme has to
10
+ * be resolved in JavaScript — reading the same signals the host's own CSS reads
11
+ * — and applied to an attribute inside the widget. That is what this module
12
+ * does, and why it is a plain DOM module rather than a stylesheet.
13
+ */
14
+ type ChatWidgetTheme = "auto" | "dark" | "light";
15
+ type ResolvedTheme = "dark" | "light";
16
+ interface ResolveThemeOptions {
17
+ /** Injection seam for tests. Defaults to the ambient document/window. */
18
+ root?: Element | null;
19
+ view?: Window | null;
20
+ }
21
+ /**
22
+ * Resolve a requested theme to the one the widget should actually paint.
23
+ *
24
+ * Falls back to `light` when there is nothing to read — during SSR there is no
25
+ * document, and guessing dark would flash the wrong palette on hydration for
26
+ * the majority of pages.
27
+ */
28
+ declare function resolveTheme(theme: ChatWidgetTheme, {
29
+ root,
30
+ view
31
+ }?: ResolveThemeOptions): ResolvedTheme;
32
+ //#endregion
4
33
  //#region src/types.d.ts
5
34
  type WidgetSenderType = "visitor" | "operator" | "system";
6
35
  interface WidgetConfig {
@@ -124,6 +153,18 @@ interface ChatWidgetProps {
124
153
  initiallyOpen?: boolean;
125
154
  launcherLabel?: string;
126
155
  storage?: Pick<Storage, "getItem" | "setItem">;
156
+ /**
157
+ * Inline styles on the widget root. The intended use is overriding palette
158
+ * tokens — `{ "--clivly-chat-panel": "#0b0f19" }` — which works because
159
+ * inline declarations beat both palette rules without any specificity games.
160
+ */
161
+ style?: CSSProperties;
162
+ /**
163
+ * `"auto"` (the default) follows the host app: a `data-theme` attribute or a
164
+ * `dark`/`light` class on `<html>`, then the OS preference. `"light"` and
165
+ * `"dark"` pin the widget regardless of what the host is doing.
166
+ */
167
+ theme?: ChatWidgetTheme;
127
168
  widgetId: string;
128
169
  }
129
170
  declare function ChatWidget({
@@ -133,6 +174,8 @@ declare function ChatWidget({
133
174
  initiallyOpen,
134
175
  launcherLabel,
135
176
  storage,
177
+ style,
178
+ theme,
136
179
  widgetId
137
180
  }: ChatWidgetProps): ReactElement;
138
181
  declare function ChatLauncher(): null;
@@ -148,16 +191,20 @@ interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" |
148
191
  socketUrlTemplate?: string;
149
192
  widgetId: string;
150
193
  }
151
- declare function mountChatWidget$1({
152
- container,
153
- createTransport,
154
- sessionUrl,
155
- socketUrlTemplate,
156
- widgetId,
157
- ...props
158
- }: MountChatWidgetOptions$1): Root;
159
- interface ScriptMountResult$1 {
194
+ interface ChatWidgetMount$1 {
160
195
  root: Root;
196
+ /**
197
+ * Repaint the widget in a different theme. `"auto"` resumes following the
198
+ * host, so a caller can hand control back as easily as taking it.
199
+ *
200
+ * Only needed by hosts whose theme toggle does not touch `<html>` — the
201
+ * default `"auto"` already tracks a `data-theme` attribute or a `dark`
202
+ * class there.
203
+ */
204
+ setTheme: (theme: ChatWidgetTheme) => void;
205
+ }
206
+ declare function mountChatWidget$1(options: MountChatWidgetOptions$1): Root;
207
+ interface ScriptMountResult$1 extends ChatWidgetMount$1 {
161
208
  shadowRoot: ShadowRoot;
162
209
  }
163
210
  declare function mountChatWidgetScript$1(script?: HTMLScriptElement): ScriptMountResult$1;
@@ -187,6 +234,7 @@ declare function createSessionFetcher({
187
234
  }: CreateSessionFetcherOptions): GetSession;
188
235
  //#endregion
189
236
  //#region src/index.d.ts
237
+ type ChatWidgetMount = ChatWidgetMount$1;
190
238
  type MountChatWidgetOptions = MountChatWidgetOptions$1;
191
239
  type ScriptMountResult = ScriptMountResult$1;
192
240
  declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
@@ -195,4 +243,4 @@ declare const createClivlyTransport: typeof createClivlyTransport$1;
195
243
  declare const mountChatWidget: typeof mountChatWidget$1;
196
244
  declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
197
245
  //#endregion
198
- export { ChatLauncher, ChatPanel, type ChatTransportConnection, type ChatTransportHandlers, ChatWidget, type ChatWidgetVisitor, ConversationView, type CreateChatTransport, type CreateChatTransportOptions, CreateClivlyTransportOptions, type CreateLoopbackTransportOptions, type CreateWebSocketTransportOptions, type GetSession, type GetSessionOptions, MessageComposer, MountChatWidgetOptions, PreChatForm, ScriptMountResult, type WidgetClientCreateMessageEvent, type WidgetClientEvent, type WidgetConfig, type WidgetContact, type WidgetConversation, type WidgetConversationSnapshotEvent, type WidgetErrorEvent, type WidgetMessage, type WidgetMessageAckEvent, type WidgetMessageCreatedEvent, type WidgetSenderType, type WidgetServerEvent, type WidgetSessionPayload, type WidgetSessionReadyEvent, type WidgetSummary, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript };
246
+ export { ChatLauncher, ChatPanel, type ChatTransportConnection, type ChatTransportHandlers, ChatWidget, ChatWidgetMount, type ChatWidgetProps, type ChatWidgetTheme, type ChatWidgetVisitor, ConversationView, type CreateChatTransport, type CreateChatTransportOptions, CreateClivlyTransportOptions, type CreateLoopbackTransportOptions, type CreateWebSocketTransportOptions, type GetSession, type GetSessionOptions, MessageComposer, MountChatWidgetOptions, PreChatForm, type ResolvedTheme, ScriptMountResult, type WidgetClientCreateMessageEvent, type WidgetClientEvent, type WidgetConfig, type WidgetContact, type WidgetConversation, type WidgetConversationSnapshotEvent, type WidgetErrorEvent, type WidgetMessage, type WidgetMessageAckEvent, type WidgetMessageCreatedEvent, type WidgetSenderType, type WidgetServerEvent, type WidgetSessionPayload, type WidgetSessionReadyEvent, type WidgetSummary, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript, resolveTheme };
package/dist/index.mjs CHANGED
@@ -1,21 +1,162 @@
1
1
  import { createRoot } from "react-dom/client";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/theme.ts
5
+ const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
6
+ /** The attribute set by next-themes, daisyUI, and most `data-*` theme setups. */
7
+ const THEME_ATTRIBUTE = "data-theme";
8
+ /** Attributes worth re-resolving on. Anything else on `<html>` is noise. */
9
+ const OBSERVED_ATTRIBUTES = ["class", THEME_ATTRIBUTE];
10
+ function isResolvedTheme(value) {
11
+ return value === "dark" || value === "light";
12
+ }
13
+ /**
14
+ * The host's explicit signals, in precedence order: a `data-theme` attribute
15
+ * beats a class, because a host that sets both is using the attribute as the
16
+ * source of truth and the class as a styling convenience.
17
+ *
18
+ * Both conventions are checked for `light` as well as `dark`. A host that says
19
+ * `class="light"` on an OS-dark machine means it, and the explicit signal
20
+ * should win over the OS preference below it.
21
+ */
22
+ function readHostTheme(root) {
23
+ const attribute = root.getAttribute(THEME_ATTRIBUTE);
24
+ if (isResolvedTheme(attribute)) return attribute;
25
+ if (root.classList.contains("dark")) return "dark";
26
+ if (root.classList.contains("light")) return "light";
27
+ return null;
28
+ }
29
+ function prefersDark(view) {
30
+ return view.matchMedia?.(DARK_MEDIA_QUERY).matches ?? false;
31
+ }
32
+ function defaultRoot() {
33
+ return typeof document === "undefined" ? null : document.documentElement;
34
+ }
35
+ function defaultView() {
36
+ return typeof window === "undefined" ? null : window;
37
+ }
38
+ /**
39
+ * Resolve a requested theme to the one the widget should actually paint.
40
+ *
41
+ * Falls back to `light` when there is nothing to read — during SSR there is no
42
+ * document, and guessing dark would flash the wrong palette on hydration for
43
+ * the majority of pages.
44
+ */
45
+ function resolveTheme(theme, { root = defaultRoot(), view = defaultView() } = {}) {
46
+ if (theme !== "auto") return theme;
47
+ const hostTheme = root ? readHostTheme(root) : null;
48
+ if (hostTheme) return hostTheme;
49
+ if (view && prefersDark(view)) return "dark";
50
+ return "light";
51
+ }
52
+ /**
53
+ * Watch both host signals and call `onChange` whenever the resolved theme
54
+ * changes. Returns an unsubscribe function.
55
+ *
56
+ * An explicit `light`/`dark` subscribes to nothing — there is no signal that
57
+ * could change the answer — so callers can pass any theme without branching.
58
+ */
59
+ function observeTheme(theme, onChange, { root = defaultRoot(), view = defaultView() } = {}) {
60
+ if (theme !== "auto") return () => {};
61
+ let current = resolveTheme(theme, {
62
+ root,
63
+ view
64
+ });
65
+ const publish = () => {
66
+ const next = resolveTheme(theme, {
67
+ root,
68
+ view
69
+ });
70
+ if (next !== current) {
71
+ current = next;
72
+ onChange(next);
73
+ }
74
+ };
75
+ const teardowns = [];
76
+ if (root && typeof MutationObserver !== "undefined") {
77
+ const observer = new MutationObserver(publish);
78
+ observer.observe(root, {
79
+ attributeFilter: OBSERVED_ATTRIBUTES,
80
+ attributes: true
81
+ });
82
+ teardowns.push(() => {
83
+ observer.disconnect();
84
+ });
85
+ }
86
+ const media = view?.matchMedia?.(DARK_MEDIA_QUERY);
87
+ if (media) {
88
+ if (media.addEventListener) {
89
+ media.addEventListener("change", publish);
90
+ teardowns.push(() => {
91
+ media.removeEventListener("change", publish);
92
+ });
93
+ } else if (media.addListener) {
94
+ media.addListener(publish);
95
+ teardowns.push(() => {
96
+ media.removeListener(publish);
97
+ });
98
+ }
99
+ }
100
+ return () => {
101
+ for (const teardown of teardowns) teardown();
102
+ };
103
+ }
104
+ /**
105
+ * Narrow an untrusted string — a `data-theme` script attribute, a `setTheme`
106
+ * call from plain JS — to a theme, falling back to `auto`.
107
+ */
108
+ function parseTheme(value) {
109
+ if (value === "auto" || value === "dark" || value === "light") return value;
110
+ return "auto";
111
+ }
112
+ //#endregion
4
113
  //#region src/chat-widget.tsx
114
+ const DELIVERY_LABELS = {
115
+ failed: "Failed to send",
116
+ pending: "Sending…",
117
+ sent: null
118
+ };
5
119
  const WIDGET_STYLES = `
6
120
  .clivly-chat-widget, .clivly-chat-widget * { box-sizing: border-box; }
7
121
  .clivly-chat-widget {
8
122
  --clivly-chat-accent: #185adb;
123
+ --clivly-chat-accent-contrast: #ffffff;
9
124
  --clivly-chat-panel: #ffffff;
125
+ --clivly-chat-input: transparent;
10
126
  --clivly-chat-border: #d6dde8;
11
127
  --clivly-chat-text: #132238;
12
128
  --clivly-chat-muted: #60738a;
13
129
  --clivly-chat-visitor: #ecf3ff;
14
130
  --clivly-chat-operator: #f5f7fa;
131
+ --clivly-chat-shadow: rgba(19, 34, 56, 0.18);
132
+ --clivly-chat-error-bg: #fff1f1;
133
+ --clivly-chat-error-border: #f2c2c2;
134
+ --clivly-chat-error-text: #9f2d2d;
15
135
  color: var(--clivly-chat-text);
136
+ color-scheme: light;
16
137
  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
138
  position: relative;
18
139
  }
140
+ /* Dark is a pure token override — every rule below reads through a variable, so
141
+ the palette is the only thing that changes between themes. The color-scheme
142
+ declaration carries the theme to the parts of the UI that CSS cannot reach:
143
+ native scrollbars, autofill backgrounds, and the caret. */
144
+ .clivly-chat-widget[data-clivly-theme="dark"] {
145
+ --clivly-chat-accent: #4f8cff;
146
+ --clivly-chat-accent-contrast: #0b1220;
147
+ --clivly-chat-panel: #111827;
148
+ --clivly-chat-input: #0b1220;
149
+ --clivly-chat-border: #2a3546;
150
+ --clivly-chat-text: #e8edf5;
151
+ --clivly-chat-muted: #93a2b8;
152
+ --clivly-chat-visitor: #1e3a6b;
153
+ --clivly-chat-operator: #1b2534;
154
+ --clivly-chat-shadow: rgba(0, 0, 0, 0.5);
155
+ --clivly-chat-error-bg: #2c1618;
156
+ --clivly-chat-error-border: #5c2a2e;
157
+ --clivly-chat-error-text: #ffb4b4;
158
+ color-scheme: dark;
159
+ }
19
160
  .clivly-chat-widget button,
20
161
  .clivly-chat-widget input,
21
162
  .clivly-chat-widget textarea {
@@ -26,7 +167,7 @@ const WIDGET_STYLES = `
26
167
  background: var(--clivly-chat-accent);
27
168
  border: 0;
28
169
  border-radius: 999px;
29
- color: white;
170
+ color: var(--clivly-chat-accent-contrast);
30
171
  cursor: pointer;
31
172
  display: inline-flex;
32
173
  gap: 0.5rem;
@@ -36,7 +177,7 @@ const WIDGET_STYLES = `
36
177
  background: var(--clivly-chat-panel);
37
178
  border: 1px solid var(--clivly-chat-border);
38
179
  border-radius: 1rem;
39
- box-shadow: 0 24px 80px rgba(19, 34, 56, 0.18);
180
+ box-shadow: 0 24px 80px var(--clivly-chat-shadow);
40
181
  display: grid;
41
182
  grid-template-rows: auto 1fr auto;
42
183
  height: min(44rem, 75vh);
@@ -139,37 +280,54 @@ const WIDGET_STYLES = `
139
280
  }
140
281
  .clivly-chat-widget__input,
141
282
  .clivly-chat-widget__textarea {
283
+ background: var(--clivly-chat-input);
142
284
  border: 1px solid var(--clivly-chat-border);
143
285
  border-radius: 0.8rem;
286
+ color: inherit;
144
287
  min-width: 0;
145
288
  padding: 0.75rem 0.85rem;
146
289
  }
290
+ .clivly-chat-widget__input::placeholder,
291
+ .clivly-chat-widget__textarea::placeholder {
292
+ color: var(--clivly-chat-muted);
293
+ }
147
294
  .clivly-chat-widget__textarea {
148
295
  min-height: 5.5rem;
149
296
  resize: vertical;
150
297
  }
151
298
  .clivly-chat-widget__actions {
299
+ align-items: center;
152
300
  display: flex;
153
301
  gap: 0.75rem;
154
302
  justify-content: space-between;
155
303
  }
304
+ /* The subtitle is the only part that may reflow; without min-width the flex
305
+ default (min-content) lets it push the button below its intrinsic width. */
306
+ .clivly-chat-widget__actions .clivly-chat-widget__subtitle {
307
+ min-width: 0;
308
+ }
156
309
  .clivly-chat-widget__button {
157
310
  background: var(--clivly-chat-accent);
158
311
  border: 0;
159
312
  border-radius: 0.8rem;
160
- color: white;
313
+ color: var(--clivly-chat-accent-contrast);
161
314
  cursor: pointer;
315
+ flex: 0 0 auto;
316
+ font: inherit;
317
+ font-weight: 600;
318
+ line-height: 1.2;
162
319
  padding: 0.75rem 0.95rem;
320
+ white-space: nowrap;
163
321
  }
164
322
  .clivly-chat-widget__button[disabled] {
165
323
  cursor: not-allowed;
166
324
  opacity: 0.55;
167
325
  }
168
326
  .clivly-chat-widget__error {
169
- background: #fff1f1;
170
- border: 1px solid #f2c2c2;
327
+ background: var(--clivly-chat-error-bg);
328
+ border: 1px solid var(--clivly-chat-error-border);
171
329
  border-radius: 0.8rem;
172
- color: #9f2d2d;
330
+ color: var(--clivly-chat-error-text);
173
331
  font-size: 0.875rem;
174
332
  padding: 0.75rem;
175
333
  }
@@ -231,14 +389,93 @@ function markMessageFailed(messages, clientMessageId) {
231
389
  function sortMessages(messages) {
232
390
  return [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
233
391
  }
234
- function useThemeStyle(session) {
392
+ function useThemeStyle(session, style) {
235
393
  return useMemo(() => {
236
394
  const brandColor = session?.widget.config.brandColor ?? null;
237
- if (!brandColor) return;
238
- return { ["--clivly-chat-accent"]: brandColor };
239
- }, [session?.widget.config.brandColor]);
395
+ if (!brandColor) return style;
396
+ return {
397
+ ["--clivly-chat-accent"]: brandColor,
398
+ ...style
399
+ };
400
+ }, [session?.widget.config.brandColor, style]);
401
+ }
402
+ /**
403
+ * Track the theme the widget should paint.
404
+ *
405
+ * The initial value is resolved synchronously so the first paint is already
406
+ * correct — deferring it to an effect would flash light on a dark page.
407
+ */
408
+ function useResolvedTheme(theme) {
409
+ const [resolved, setResolved] = useState(() => resolveTheme(theme));
410
+ useEffect(() => {
411
+ setResolved(resolveTheme(theme));
412
+ return observeTheme(theme, setResolved);
413
+ }, [theme]);
414
+ return resolved;
415
+ }
416
+ /**
417
+ * Reduce one socket event into widget state.
418
+ *
419
+ * Lives at module scope rather than inside the connect effect so the wire
420
+ * protocol reads as a single unit, and so `ChatWidget` stays inside the
421
+ * cognitive-complexity budget.
422
+ */
423
+ function applyServerEvent(event, { setError, setIsReady, setMessages }) {
424
+ switch (event.type) {
425
+ case "conversation.snapshot":
426
+ setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
427
+ return;
428
+ case "error":
429
+ setError(event.message);
430
+ return;
431
+ case "message.ack":
432
+ setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, event.clientMessageId, event.message)));
433
+ return;
434
+ case "message.created": {
435
+ const clientMessageId = event.clientMessageId;
436
+ setMessages((currentMessages) => sortMessages(clientMessageId ? replacePendingMessage(currentMessages, clientMessageId, event.message) : mergeMessage(currentMessages, event.message)));
437
+ return;
438
+ }
439
+ case "session.ready":
440
+ setIsReady(true);
441
+ return;
442
+ default: return;
443
+ }
444
+ }
445
+ function MessageBubble({ message }) {
446
+ return /* @__PURE__ */ jsxs("article", {
447
+ className: "clivly-chat-widget__message",
448
+ "data-sender": message.senderType,
449
+ children: [
450
+ /* @__PURE__ */ jsxs("div", {
451
+ className: "clivly-chat-widget__message-meta",
452
+ children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
453
+ }),
454
+ /* @__PURE__ */ jsx("div", { children: message.body }),
455
+ message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
456
+ className: "clivly-chat-widget__status",
457
+ children: DELIVERY_LABELS[message.delivery]
458
+ })
459
+ ]
460
+ });
461
+ }
462
+ /**
463
+ * The scrollable transcript. Split out of `ChatWidget` because the three states
464
+ * (no session, empty thread, messages) were a nested ternary in the render tree
465
+ * — early returns make them readable and keep the parent's complexity down.
466
+ */
467
+ function MessageList({ greeting, hasSession, messages }) {
468
+ if (!hasSession) return /* @__PURE__ */ jsx("p", {
469
+ className: "clivly-chat-widget__empty",
470
+ children: greeting
471
+ });
472
+ if (messages.length === 0) return /* @__PURE__ */ jsx("p", {
473
+ className: "clivly-chat-widget__empty",
474
+ children: "No messages yet. Say hello to get the conversation started."
475
+ });
476
+ return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
240
477
  }
241
- function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, widgetId }) {
478
+ function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
242
479
  const [isOpen, setIsOpen] = useState(initiallyOpen);
243
480
  const [error, setError] = useState(null);
244
481
  const [isConnecting, setIsConnecting] = useState(false);
@@ -252,7 +489,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
252
489
  });
253
490
  const transportRef = useRef(null);
254
491
  const listRef = useRef(null);
255
- const themeStyle = useThemeStyle(session);
492
+ const themeStyle = useThemeStyle(session, style);
493
+ const resolvedTheme = useResolvedTheme(theme);
256
494
  useEffect(() => () => {
257
495
  transportRef.current?.close();
258
496
  transportRef.current = null;
@@ -266,31 +504,6 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
266
504
  transportRef.current = null;
267
505
  setIsReady(false);
268
506
  if (!(session && createTransport)) return;
269
- const handleEvent = (event) => {
270
- switch (event.type) {
271
- case "conversation.snapshot":
272
- setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
273
- return;
274
- case "error":
275
- setError(event.message);
276
- return;
277
- case "message.ack":
278
- setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, event.clientMessageId, event.message)));
279
- return;
280
- case "message.created": {
281
- const clientMessageId = event.clientMessageId;
282
- if (clientMessageId) {
283
- setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, clientMessageId, event.message)));
284
- return;
285
- }
286
- setMessages((currentMessages) => sortMessages(mergeMessage(currentMessages, event.message)));
287
- return;
288
- }
289
- case "session.ready":
290
- setIsReady(true);
291
- return;
292
- }
293
- };
294
507
  const connection = createTransport({
295
508
  handlers: {
296
509
  onClose: () => {
@@ -299,7 +512,13 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
299
512
  onError: (transportError) => {
300
513
  setError(transportError.message);
301
514
  },
302
- onEvent: handleEvent,
515
+ onEvent: (event) => {
516
+ applyServerEvent(event, {
517
+ setError,
518
+ setIsReady,
519
+ setMessages
520
+ });
521
+ },
303
522
  onOpen: () => {
304
523
  setIsReady(true);
305
524
  }
@@ -367,6 +586,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
367
586
  const sessionSummary = session ? `${session.contact.name} • ${session.contact.email}` : greeting;
368
587
  return /* @__PURE__ */ jsxs("div", {
369
588
  className: `clivly-chat-widget ${className ?? ""}`.trim(),
589
+ "data-clivly-theme": resolvedTheme,
370
590
  style: themeStyle,
371
591
  children: [
372
592
  /* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
@@ -406,26 +626,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
406
626
  children: [error ? /* @__PURE__ */ jsx("div", {
407
627
  className: "clivly-chat-widget__error",
408
628
  children: error
409
- }) : null, session ? messages.length > 0 ? messages.map((message) => /* @__PURE__ */ jsxs("article", {
410
- className: "clivly-chat-widget__message",
411
- "data-sender": message.senderType,
412
- children: [
413
- /* @__PURE__ */ jsxs("div", {
414
- className: "clivly-chat-widget__message-meta",
415
- children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
416
- }),
417
- /* @__PURE__ */ jsx("div", { children: message.body }),
418
- message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
419
- className: "clivly-chat-widget__status",
420
- children: message.delivery === "pending" ? "Sending…" : "Failed to send"
421
- })
422
- ]
423
- }, message.id)) : /* @__PURE__ */ jsx("p", {
424
- className: "clivly-chat-widget__empty",
425
- children: "No messages yet. Say hello to get the conversation started."
426
- }) : /* @__PURE__ */ jsx("p", {
427
- className: "clivly-chat-widget__empty",
428
- children: greeting
629
+ }) : null, /* @__PURE__ */ jsx(MessageList, {
630
+ greeting,
631
+ hasSession: Boolean(session),
632
+ messages
429
633
  })]
430
634
  }),
431
635
  session ? /* @__PURE__ */ jsxs("form", {
@@ -688,16 +892,27 @@ const MOUNTED_DATA_ATTR = "clivlyMounted";
688
892
  function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
689
893
  return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
690
894
  }
691
- function mountChatWidget$1({ container, createTransport, sessionUrl, socketUrlTemplate, widgetId, ...props }) {
895
+ function createWidgetMount({ container, createTransport, sessionUrl, socketUrlTemplate, theme = "auto", widgetId, ...props }) {
692
896
  const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport$1());
897
+ const getSession = createSessionFetcher({ sessionUrl });
693
898
  const root = createRoot(container);
694
- root.render(/* @__PURE__ */ jsx(ChatWidget, {
695
- ...props,
696
- createTransport: transport,
697
- getSession: createSessionFetcher({ sessionUrl }),
698
- widgetId
699
- }));
700
- return root;
899
+ const render = (nextTheme) => {
900
+ root.render(/* @__PURE__ */ jsx(ChatWidget, {
901
+ ...props,
902
+ createTransport: transport,
903
+ getSession,
904
+ theme: nextTheme,
905
+ widgetId
906
+ }));
907
+ };
908
+ render(theme);
909
+ return {
910
+ root,
911
+ setTheme: render
912
+ };
913
+ }
914
+ function mountChatWidget$1(options) {
915
+ return createWidgetMount(options).root;
701
916
  }
702
917
  function markScriptMounted(script) {
703
918
  script.dataset[MOUNTED_DATA_ATTR] = "true";
@@ -716,15 +931,17 @@ function mountChatWidgetScript$1(script = document.currentScript) {
716
931
  const shadowRoot = host.attachShadow({ mode: "open" });
717
932
  const container = document.createElement("div");
718
933
  shadowRoot.append(container);
719
- const root = mountChatWidget$1({
934
+ const { root, setTheme } = createWidgetMount({
720
935
  container,
721
936
  sessionUrl,
722
937
  socketUrlTemplate: script.dataset.socketUrlTemplate,
938
+ theme: parseTheme(script.dataset.theme),
723
939
  widgetId
724
940
  });
725
941
  markScriptMounted(script);
726
942
  return {
727
943
  root,
944
+ setTheme,
728
945
  shadowRoot
729
946
  };
730
947
  }
@@ -739,4 +956,4 @@ const createClivlyTransport = createClivlyTransport$1;
739
956
  const mountChatWidget = mountChatWidget$1;
740
957
  const mountChatWidgetScript = mountChatWidgetScript$1;
741
958
  //#endregion
742
- export { ChatLauncher, ChatPanel, ChatWidget, ConversationView, MessageComposer, PreChatForm, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript };
959
+ export { ChatLauncher, ChatPanel, ChatWidget, ConversationView, MessageComposer, PreChatForm, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript, resolveTheme };