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