@clivly/chat-widget 0.4.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/browser.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
+ }
241
445
  }
242
- function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, widgetId }) {
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)) });
478
+ }
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", {
@@ -617,16 +821,27 @@ const MOUNTED_DATA_ATTR = "clivlyMounted";
617
821
  function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
618
822
  return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
619
823
  }
620
- function mountChatWidget$1({ container, createTransport, sessionUrl, socketUrlTemplate, widgetId, ...props }) {
824
+ function createWidgetMount({ container, createTransport, sessionUrl, socketUrlTemplate, theme = "auto", widgetId, ...props }) {
621
825
  const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport());
826
+ const getSession = createSessionFetcher({ sessionUrl });
622
827
  const root = (0, react_dom_client.createRoot)(container);
623
- root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatWidget, {
624
- ...props,
625
- createTransport: transport,
626
- getSession: createSessionFetcher({ sessionUrl }),
627
- widgetId
628
- }));
629
- return root;
828
+ const render = (nextTheme) => {
829
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatWidget, {
830
+ ...props,
831
+ createTransport: transport,
832
+ getSession,
833
+ theme: nextTheme,
834
+ widgetId
835
+ }));
836
+ };
837
+ render(theme);
838
+ return {
839
+ root,
840
+ setTheme: render
841
+ };
842
+ }
843
+ function mountChatWidget$1(options) {
844
+ return createWidgetMount(options).root;
630
845
  }
631
846
  function markScriptMounted(script) {
632
847
  script.dataset[MOUNTED_DATA_ATTR] = "true";
@@ -645,15 +860,17 @@ function mountChatWidgetScript$1(script = document.currentScript) {
645
860
  const shadowRoot = host.attachShadow({ mode: "open" });
646
861
  const container = document.createElement("div");
647
862
  shadowRoot.append(container);
648
- const root = mountChatWidget$1({
863
+ const { root, setTheme } = createWidgetMount({
649
864
  container,
650
865
  sessionUrl,
651
866
  socketUrlTemplate: script.dataset.socketUrlTemplate,
867
+ theme: parseTheme(script.dataset.theme),
652
868
  widgetId
653
869
  });
654
870
  markScriptMounted(script);
655
871
  return {
656
872
  root,
873
+ setTheme,
657
874
  shadowRoot
658
875
  };
659
876
  }
@@ -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,26 +137,31 @@ interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" |
111
137
  socketUrlTemplate?: string;
112
138
  widgetId: string;
113
139
  }
114
- declare function mountChatWidget$1({
115
- container,
116
- createTransport,
117
- sessionUrl,
118
- socketUrlTemplate,
119
- widgetId,
120
- ...props
121
- }: MountChatWidgetOptions$1): Root;
122
- interface ScriptMountResult$1 {
140
+ interface ChatWidgetMount$1 {
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$1(options: MountChatWidgetOptions$1): Root;
153
+ interface ScriptMountResult$1 extends ChatWidgetMount$1 {
124
154
  shadowRoot: ShadowRoot;
125
155
  }
126
156
  declare function mountChatWidgetScript$1(script?: HTMLScriptElement): ScriptMountResult$1;
127
157
  declare function autoMountChatWidgetScript$1(script?: HTMLScriptElement | null): ScriptMountResult$1 | null;
128
158
  //#endregion
129
159
  //#region src/browser.d.ts
160
+ type ChatWidgetMount = ChatWidgetMount$1;
130
161
  type MountChatWidgetOptions = MountChatWidgetOptions$1;
131
162
  type ScriptMountResult = ScriptMountResult$1;
132
163
  declare const mountChatWidget: typeof mountChatWidget$1;
133
164
  declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
134
165
  declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
135
166
  //#endregion
136
- export { MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
167
+ export { ChatWidgetMount, MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
@@ -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,26 +137,31 @@ interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" |
111
137
  socketUrlTemplate?: string;
112
138
  widgetId: string;
113
139
  }
114
- declare function mountChatWidget$1({
115
- container,
116
- createTransport,
117
- sessionUrl,
118
- socketUrlTemplate,
119
- widgetId,
120
- ...props
121
- }: MountChatWidgetOptions$1): Root;
122
- interface ScriptMountResult$1 {
140
+ interface ChatWidgetMount$1 {
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$1(options: MountChatWidgetOptions$1): Root;
153
+ interface ScriptMountResult$1 extends ChatWidgetMount$1 {
124
154
  shadowRoot: ShadowRoot;
125
155
  }
126
156
  declare function mountChatWidgetScript$1(script?: HTMLScriptElement): ScriptMountResult$1;
127
157
  declare function autoMountChatWidgetScript$1(script?: HTMLScriptElement | null): ScriptMountResult$1 | null;
128
158
  //#endregion
129
159
  //#region src/browser.d.ts
160
+ type ChatWidgetMount = ChatWidgetMount$1;
130
161
  type MountChatWidgetOptions = MountChatWidgetOptions$1;
131
162
  type ScriptMountResult = ScriptMountResult$1;
132
163
  declare const mountChatWidget: typeof mountChatWidget$1;
133
164
  declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
134
165
  declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
135
166
  //#endregion
136
- export { MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
167
+ export { ChatWidgetMount, MountChatWidgetOptions, ScriptMountResult, autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };