@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.
@@ -1,4 +1,18 @@
1
1
  import { Root } from "react-dom/client";
2
+ import { CSSProperties } from "react";
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
+ //#endregion
2
16
  //#region src/types.d.ts
3
17
  type WidgetSenderType = "visitor" | "operator" | "system";
4
18
  interface WidgetConfig {
@@ -101,6 +115,18 @@ interface ChatWidgetProps {
101
115
  initiallyOpen?: boolean;
102
116
  launcherLabel?: string;
103
117
  storage?: Pick<Storage, "getItem" | "setItem">;
118
+ /**
119
+ * Inline styles on the widget root. The intended use is overriding palette
120
+ * tokens — `{ "--clivly-chat-panel": "#0b0f19" }` — which works because
121
+ * inline declarations beat both palette rules without any specificity games.
122
+ */
123
+ style?: CSSProperties;
124
+ /**
125
+ * `"auto"` (the default) follows the host app: a `data-theme` attribute or a
126
+ * `dark`/`light` class on `<html>`, then the OS preference. `"light"` and
127
+ * `"dark"` pin the widget regardless of what the host is doing.
128
+ */
129
+ theme?: ChatWidgetTheme;
104
130
  widgetId: string;
105
131
  }
106
132
  //#endregion
@@ -111,19 +137,23 @@ interface MountChatWidgetOptions extends Omit<ChatWidgetProps, "getSession" | "w
111
137
  socketUrlTemplate?: string;
112
138
  widgetId: string;
113
139
  }
114
- declare function mountChatWidget({
115
- container,
116
- createTransport,
117
- sessionUrl,
118
- socketUrlTemplate,
119
- widgetId,
120
- ...props
121
- }: MountChatWidgetOptions): Root;
122
- interface ScriptMountResult {
140
+ interface ChatWidgetMount {
123
141
  root: Root;
142
+ /**
143
+ * Repaint the widget in a different theme. `"auto"` resumes following the
144
+ * host, so a caller can hand control back as easily as taking it.
145
+ *
146
+ * Only needed by hosts whose theme toggle does not touch `<html>` — the
147
+ * default `"auto"` already tracks a `data-theme` attribute or a `dark`
148
+ * class there.
149
+ */
150
+ setTheme: (theme: ChatWidgetTheme) => void;
151
+ }
152
+ declare function mountChatWidget(options: MountChatWidgetOptions): Root;
153
+ interface ScriptMountResult extends ChatWidgetMount {
124
154
  shadowRoot: ShadowRoot;
125
155
  }
126
156
  declare function mountChatWidgetScript(script?: HTMLScriptElement): ScriptMountResult;
127
157
  declare function autoMountChatWidgetScript(script?: HTMLScriptElement | null): ScriptMountResult | null;
128
158
  //#endregion
129
- export { MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
159
+ export { ChatWidgetMount, MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
@@ -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
+ }
240
444
  }
241
- function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, widgetId }) {
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)) });
477
+ }
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", {
@@ -616,16 +820,27 @@ const MOUNTED_DATA_ATTR = "clivlyMounted";
616
820
  function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
617
821
  return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
618
822
  }
619
- function mountChatWidget({ container, createTransport, sessionUrl, socketUrlTemplate, widgetId, ...props }) {
823
+ function createWidgetMount({ container, createTransport, sessionUrl, socketUrlTemplate, theme = "auto", widgetId, ...props }) {
620
824
  const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport());
825
+ const getSession = createSessionFetcher({ sessionUrl });
621
826
  const root = createRoot(container);
622
- root.render(/* @__PURE__ */ jsx(ChatWidget, {
623
- ...props,
624
- createTransport: transport,
625
- getSession: createSessionFetcher({ sessionUrl }),
626
- widgetId
627
- }));
628
- return root;
827
+ const render = (nextTheme) => {
828
+ root.render(/* @__PURE__ */ jsx(ChatWidget, {
829
+ ...props,
830
+ createTransport: transport,
831
+ getSession,
832
+ theme: nextTheme,
833
+ widgetId
834
+ }));
835
+ };
836
+ render(theme);
837
+ return {
838
+ root,
839
+ setTheme: render
840
+ };
841
+ }
842
+ function mountChatWidget(options) {
843
+ return createWidgetMount(options).root;
629
844
  }
630
845
  function markScriptMounted(script) {
631
846
  script.dataset[MOUNTED_DATA_ATTR] = "true";
@@ -644,15 +859,17 @@ function mountChatWidgetScript(script = document.currentScript) {
644
859
  const shadowRoot = host.attachShadow({ mode: "open" });
645
860
  const container = document.createElement("div");
646
861
  shadowRoot.append(container);
647
- const root = mountChatWidget({
862
+ const { root, setTheme } = createWidgetMount({
648
863
  container,
649
864
  sessionUrl,
650
865
  socketUrlTemplate: script.dataset.socketUrlTemplate,
866
+ theme: parseTheme(script.dataset.theme),
651
867
  widgetId
652
868
  });
653
869
  markScriptMounted(script);
654
870
  return {
655
871
  root,
872
+ setTheme,
656
873
  shadowRoot
657
874
  };
658
875
  }