@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/README.md +81 -4
- package/dist/browser-mount.cjs +284 -67
- package/dist/browser-mount.d.cts +40 -10
- package/dist/browser-mount.d.mts +40 -10
- package/dist/browser-mount.mjs +285 -68
- package/dist/browser.cjs +284 -67
- package/dist/browser.d.cts +41 -10
- package/dist/browser.d.mts +41 -10
- package/dist/browser.mjs +285 -68
- package/dist/index.cjs +285 -67
- package/dist/index.d.cts +59 -11
- package/dist/index.d.mts +59 -11
- package/dist/index.mjs +286 -69
- package/dist/widget.global.js +136 -82
- package/package.json +61 -49
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:
|
|
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
|
|
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:
|
|
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:
|
|
171
|
-
border: 1px solid
|
|
328
|
+
background: var(--clivly-chat-error-bg);
|
|
329
|
+
border: 1px solid var(--clivly-chat-error-border);
|
|
172
330
|
border-radius: 0.8rem;
|
|
173
|
-
color:
|
|
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 {
|
|
240
|
-
|
|
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:
|
|
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,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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 =
|
|
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
|
-
|
|
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 };
|