@clivly/chat-widget 0.10.9 → 0.11.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 +87 -4
- package/dist/browser-mount.cjs +126 -13
- package/dist/browser-mount.d.cts +25 -8
- package/dist/browser-mount.d.mts +25 -8
- package/dist/browser-mount.mjs +127 -14
- package/dist/browser.cjs +126 -13
- package/dist/browser.d.cts +25 -8
- package/dist/browser.d.mts +25 -8
- package/dist/browser.mjs +127 -14
- package/dist/index.cjs +126 -13
- package/dist/index.d.cts +37 -19
- package/dist/index.d.mts +37 -19
- package/dist/index.mjs +127 -14
- package/dist/widget.global.js +115 -83
- package/package.json +2 -2
package/dist/browser.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRoot } from "react-dom/client";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/theme.ts
|
|
5
5
|
const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
@@ -255,10 +255,42 @@ const WIDGET_STYLES = `
|
|
|
255
255
|
padding: 0.5rem 1rem;
|
|
256
256
|
text-align: center;
|
|
257
257
|
}
|
|
258
|
+
/* Only the name carries the gradient. "Powered by" is ordinary running text
|
|
259
|
+
and inherits the muted colour of the line, so the mark reads as a signature
|
|
260
|
+
rather than the whole sentence being branded. */
|
|
258
261
|
.clivly-chat-widget__powered a {
|
|
259
262
|
color: inherit;
|
|
260
263
|
text-decoration: none;
|
|
261
264
|
}
|
|
265
|
+
/* The attribution is Clivly's mark, not the host's, so it uses the stops from
|
|
266
|
+
the logo itself rather than --clivly-chat-accent: the accent is driven by
|
|
267
|
+
each customer's own brandColor, and a signature that changed colour per
|
|
268
|
+
customer would stop reading as a signature.
|
|
269
|
+
|
|
270
|
+
The color declaration is the fallback and must stay a real colour:
|
|
271
|
+
background-clip is painted over it, so anything that drops the background —
|
|
272
|
+
forced-colors, an older engine, a copied-out snippet — degrades to solid
|
|
273
|
+
coral rather than to invisible text. */
|
|
274
|
+
.clivly-chat-widget__powered-mark {
|
|
275
|
+
background-image: linear-gradient(90deg, #ff5a4a 0%, #c0231a 100%);
|
|
276
|
+
-webkit-background-clip: text;
|
|
277
|
+
background-clip: text;
|
|
278
|
+
color: #c0231a;
|
|
279
|
+
-webkit-text-fill-color: transparent;
|
|
280
|
+
}
|
|
281
|
+
/* Forced-colors mode strips background-image outright, which would leave the
|
|
282
|
+
text painted in a transparent fill. Hand the colour back to the UA so the
|
|
283
|
+
mark inherits the link colour the UA picked rather than re-pinning coral. */
|
|
284
|
+
@media (forced-colors: active) {
|
|
285
|
+
.clivly-chat-widget__powered a {
|
|
286
|
+
color: LinkText;
|
|
287
|
+
}
|
|
288
|
+
.clivly-chat-widget__powered-mark {
|
|
289
|
+
background-image: none;
|
|
290
|
+
color: currentColor;
|
|
291
|
+
-webkit-text-fill-color: currentColor;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
262
294
|
.clivly-chat-widget__powered a:hover,
|
|
263
295
|
.clivly-chat-widget__powered a:focus-visible {
|
|
264
296
|
text-decoration: underline;
|
|
@@ -482,7 +514,27 @@ function MessageList({ greeting, hasSession, messages }) {
|
|
|
482
514
|
});
|
|
483
515
|
return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
|
|
484
516
|
}
|
|
485
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Whether a seeded visitor is complete enough to skip the form.
|
|
519
|
+
*
|
|
520
|
+
* Both fields have to be present and non-blank: the server requires each, so
|
|
521
|
+
* auto-starting on a half-filled identity would trade a form the visitor can
|
|
522
|
+
* complete for an error they cannot.
|
|
523
|
+
*/
|
|
524
|
+
function isCompleteVisitor(visitor) {
|
|
525
|
+
return Boolean(visitor?.name?.trim() && visitor?.email?.trim());
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* What the composer says about itself. "Connecting…" claims a socket is in
|
|
529
|
+
* flight, so it is as wrong as "Conversation ready" when no transport exists —
|
|
530
|
+
* the visitor is told to wait for something that will never arrive. The third
|
|
531
|
+
* string keeps the status honest in all three states.
|
|
532
|
+
*/
|
|
533
|
+
function composerStatusText(hasTransport, isReady) {
|
|
534
|
+
if (!hasTransport) return "Chat is unavailable";
|
|
535
|
+
return isReady ? "Conversation ready" : "Connecting…";
|
|
536
|
+
}
|
|
537
|
+
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
486
538
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
487
539
|
const [error, setError] = useState(null);
|
|
488
540
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
@@ -491,11 +543,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
491
543
|
const [messages, setMessages] = useState([]);
|
|
492
544
|
const [session, setSession] = useState(null);
|
|
493
545
|
const [visitor, setVisitor] = useState({
|
|
494
|
-
email: "",
|
|
495
|
-
name: ""
|
|
546
|
+
email: defaultVisitor?.email ?? "",
|
|
547
|
+
name: defaultVisitor?.name ?? ""
|
|
496
548
|
});
|
|
497
549
|
const transportRef = useRef(null);
|
|
498
550
|
const listRef = useRef(null);
|
|
551
|
+
useEffect(() => {
|
|
552
|
+
if (createTransport) return;
|
|
553
|
+
console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
|
|
554
|
+
}, []);
|
|
499
555
|
const themeStyle = useThemeStyle(session, style);
|
|
500
556
|
const resolvedTheme = useResolvedTheme(theme);
|
|
501
557
|
useEffect(() => () => {
|
|
@@ -538,15 +594,13 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
538
594
|
if (transportRef.current === connection) transportRef.current = null;
|
|
539
595
|
};
|
|
540
596
|
}, [createTransport, session]);
|
|
541
|
-
const
|
|
542
|
-
event.preventDefault();
|
|
597
|
+
const startSession = useCallback(async (identity) => {
|
|
543
598
|
setError(null);
|
|
544
599
|
setIsConnecting(true);
|
|
545
600
|
try {
|
|
546
|
-
const existingVisitorToken = storage?.getItem(createStorageKey(widgetId)) ?? null;
|
|
547
601
|
const nextSession = await getSession({
|
|
548
|
-
visitor,
|
|
549
|
-
visitorToken:
|
|
602
|
+
visitor: identity,
|
|
603
|
+
visitorToken: storage?.getItem(createStorageKey(widgetId)) ?? null,
|
|
550
604
|
widgetId
|
|
551
605
|
});
|
|
552
606
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
@@ -558,7 +612,30 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
558
612
|
} finally {
|
|
559
613
|
setIsConnecting(false);
|
|
560
614
|
}
|
|
615
|
+
}, [
|
|
616
|
+
getSession,
|
|
617
|
+
storage,
|
|
618
|
+
widgetId
|
|
619
|
+
]);
|
|
620
|
+
const submitVisitor = async (event) => {
|
|
621
|
+
event.preventDefault();
|
|
622
|
+
await startSession(visitor);
|
|
561
623
|
};
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
if (!(isOpen && isCompleteVisitor(defaultVisitor))) return;
|
|
626
|
+
if (session || isConnecting || error) return;
|
|
627
|
+
startSession({
|
|
628
|
+
email: defaultVisitor.email.trim(),
|
|
629
|
+
name: defaultVisitor.name.trim()
|
|
630
|
+
}).catch(() => {});
|
|
631
|
+
}, [
|
|
632
|
+
defaultVisitor,
|
|
633
|
+
error,
|
|
634
|
+
isConnecting,
|
|
635
|
+
isOpen,
|
|
636
|
+
session,
|
|
637
|
+
startSession
|
|
638
|
+
]);
|
|
562
639
|
const submitMessage = (event) => {
|
|
563
640
|
event?.preventDefault();
|
|
564
641
|
if (!(session && transportRef.current)) {
|
|
@@ -590,6 +667,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
590
667
|
setError(resolved.message);
|
|
591
668
|
}
|
|
592
669
|
};
|
|
670
|
+
const canSend = Boolean(createTransport) && isReady;
|
|
593
671
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
594
672
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
595
673
|
const sessionSummary = sessionSummaryText(session, greeting);
|
|
@@ -657,7 +735,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
657
735
|
onKeyDown: (event) => {
|
|
658
736
|
if (!isSendKey(event)) return;
|
|
659
737
|
event.preventDefault();
|
|
660
|
-
if (messageDraft.trim() &&
|
|
738
|
+
if (messageDraft.trim() && canSend) submitMessage();
|
|
661
739
|
},
|
|
662
740
|
placeholder: "Write your message",
|
|
663
741
|
value: messageDraft
|
|
@@ -666,10 +744,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
666
744
|
className: "clivly-chat-widget__actions",
|
|
667
745
|
children: [/* @__PURE__ */ jsx("span", {
|
|
668
746
|
className: "clivly-chat-widget__subtitle",
|
|
669
|
-
children:
|
|
747
|
+
children: composerStatusText(Boolean(createTransport), isReady)
|
|
670
748
|
}), /* @__PURE__ */ jsx("button", {
|
|
671
749
|
className: "clivly-chat-widget__button",
|
|
672
|
-
disabled: !(messageDraft.trim() &&
|
|
750
|
+
disabled: !(messageDraft.trim() && canSend),
|
|
673
751
|
type: "submit",
|
|
674
752
|
children: "Send"
|
|
675
753
|
})]
|
|
@@ -731,11 +809,18 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
731
809
|
}),
|
|
732
810
|
/* @__PURE__ */ jsx("p", {
|
|
733
811
|
className: "clivly-chat-widget__powered",
|
|
734
|
-
children: /* @__PURE__ */
|
|
812
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
735
813
|
href: "https://clivly.com",
|
|
736
814
|
rel: "noopener",
|
|
737
815
|
target: "_blank",
|
|
738
|
-
children:
|
|
816
|
+
children: [
|
|
817
|
+
"Powered by",
|
|
818
|
+
" ",
|
|
819
|
+
/* @__PURE__ */ jsx("span", {
|
|
820
|
+
className: "clivly-chat-widget__powered-mark",
|
|
821
|
+
children: "Clivly"
|
|
822
|
+
})
|
|
823
|
+
]
|
|
739
824
|
})
|
|
740
825
|
})
|
|
741
826
|
]
|
|
@@ -826,6 +911,33 @@ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
|
|
|
826
911
|
//#endregion
|
|
827
912
|
//#region src/browser-mount.tsx
|
|
828
913
|
const MOUNTED_DATA_ATTR = "clivlyMounted";
|
|
914
|
+
/**
|
|
915
|
+
* The identity a script-tag host already knows, read off the tag.
|
|
916
|
+
*
|
|
917
|
+
* The React component takes `defaultVisitor` as a prop; a script tag has only
|
|
918
|
+
* strings, so `data-visitor-name` / `data-visitor-email` are the same seam. A
|
|
919
|
+
* host that server-renders its page can template the signed-in user's details
|
|
920
|
+
* straight into the tag and the visitor never sees the pre-chat form.
|
|
921
|
+
*
|
|
922
|
+
* Returns `undefined` rather than an empty object when neither attribute is
|
|
923
|
+
* present, so the component's own "no identity" path stays untouched. Partial
|
|
924
|
+
* values are passed through deliberately: `ChatWidget` treats one field as a
|
|
925
|
+
* prefill and still renders the form, which is the right outcome for a host
|
|
926
|
+
* that knows an email but not a name.
|
|
927
|
+
*
|
|
928
|
+
* This is self-reported identity — anyone can edit the attribute in devtools.
|
|
929
|
+
* `resolveUser` on the host backend is what makes it trustworthy; this only
|
|
930
|
+
* removes the typing.
|
|
931
|
+
*/
|
|
932
|
+
function readScriptVisitor(dataset) {
|
|
933
|
+
const email = dataset.visitorEmail?.trim();
|
|
934
|
+
const name = dataset.visitorName?.trim();
|
|
935
|
+
if (!(email || name)) return;
|
|
936
|
+
return {
|
|
937
|
+
...email ? { email } : {},
|
|
938
|
+
...name ? { name } : {}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
829
941
|
function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
|
|
830
942
|
return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
|
|
831
943
|
}
|
|
@@ -870,6 +982,7 @@ function mountChatWidgetScript$1(script = document.currentScript) {
|
|
|
870
982
|
shadowRoot.append(container);
|
|
871
983
|
const { root, setTheme } = createWidgetMount({
|
|
872
984
|
container,
|
|
985
|
+
defaultVisitor: readScriptVisitor(script.dataset),
|
|
873
986
|
sessionUrl,
|
|
874
987
|
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
875
988
|
theme: parseTheme(script.dataset.theme),
|
package/dist/index.cjs
CHANGED
|
@@ -256,10 +256,42 @@ const WIDGET_STYLES = `
|
|
|
256
256
|
padding: 0.5rem 1rem;
|
|
257
257
|
text-align: center;
|
|
258
258
|
}
|
|
259
|
+
/* Only the name carries the gradient. "Powered by" is ordinary running text
|
|
260
|
+
and inherits the muted colour of the line, so the mark reads as a signature
|
|
261
|
+
rather than the whole sentence being branded. */
|
|
259
262
|
.clivly-chat-widget__powered a {
|
|
260
263
|
color: inherit;
|
|
261
264
|
text-decoration: none;
|
|
262
265
|
}
|
|
266
|
+
/* The attribution is Clivly's mark, not the host's, so it uses the stops from
|
|
267
|
+
the logo itself rather than --clivly-chat-accent: the accent is driven by
|
|
268
|
+
each customer's own brandColor, and a signature that changed colour per
|
|
269
|
+
customer would stop reading as a signature.
|
|
270
|
+
|
|
271
|
+
The color declaration is the fallback and must stay a real colour:
|
|
272
|
+
background-clip is painted over it, so anything that drops the background —
|
|
273
|
+
forced-colors, an older engine, a copied-out snippet — degrades to solid
|
|
274
|
+
coral rather than to invisible text. */
|
|
275
|
+
.clivly-chat-widget__powered-mark {
|
|
276
|
+
background-image: linear-gradient(90deg, #ff5a4a 0%, #c0231a 100%);
|
|
277
|
+
-webkit-background-clip: text;
|
|
278
|
+
background-clip: text;
|
|
279
|
+
color: #c0231a;
|
|
280
|
+
-webkit-text-fill-color: transparent;
|
|
281
|
+
}
|
|
282
|
+
/* Forced-colors mode strips background-image outright, which would leave the
|
|
283
|
+
text painted in a transparent fill. Hand the colour back to the UA so the
|
|
284
|
+
mark inherits the link colour the UA picked rather than re-pinning coral. */
|
|
285
|
+
@media (forced-colors: active) {
|
|
286
|
+
.clivly-chat-widget__powered a {
|
|
287
|
+
color: LinkText;
|
|
288
|
+
}
|
|
289
|
+
.clivly-chat-widget__powered-mark {
|
|
290
|
+
background-image: none;
|
|
291
|
+
color: currentColor;
|
|
292
|
+
-webkit-text-fill-color: currentColor;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
263
295
|
.clivly-chat-widget__powered a:hover,
|
|
264
296
|
.clivly-chat-widget__powered a:focus-visible {
|
|
265
297
|
text-decoration: underline;
|
|
@@ -483,7 +515,27 @@ function MessageList({ greeting, hasSession, messages }) {
|
|
|
483
515
|
});
|
|
484
516
|
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)) });
|
|
485
517
|
}
|
|
486
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Whether a seeded visitor is complete enough to skip the form.
|
|
520
|
+
*
|
|
521
|
+
* Both fields have to be present and non-blank: the server requires each, so
|
|
522
|
+
* auto-starting on a half-filled identity would trade a form the visitor can
|
|
523
|
+
* complete for an error they cannot.
|
|
524
|
+
*/
|
|
525
|
+
function isCompleteVisitor(visitor) {
|
|
526
|
+
return Boolean(visitor?.name?.trim() && visitor?.email?.trim());
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* What the composer says about itself. "Connecting…" claims a socket is in
|
|
530
|
+
* flight, so it is as wrong as "Conversation ready" when no transport exists —
|
|
531
|
+
* the visitor is told to wait for something that will never arrive. The third
|
|
532
|
+
* string keeps the status honest in all three states.
|
|
533
|
+
*/
|
|
534
|
+
function composerStatusText(hasTransport, isReady) {
|
|
535
|
+
if (!hasTransport) return "Chat is unavailable";
|
|
536
|
+
return isReady ? "Conversation ready" : "Connecting…";
|
|
537
|
+
}
|
|
538
|
+
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
487
539
|
const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
|
|
488
540
|
const [error, setError] = (0, react.useState)(null);
|
|
489
541
|
const [isConnecting, setIsConnecting] = (0, react.useState)(false);
|
|
@@ -492,11 +544,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
492
544
|
const [messages, setMessages] = (0, react.useState)([]);
|
|
493
545
|
const [session, setSession] = (0, react.useState)(null);
|
|
494
546
|
const [visitor, setVisitor] = (0, react.useState)({
|
|
495
|
-
email: "",
|
|
496
|
-
name: ""
|
|
547
|
+
email: defaultVisitor?.email ?? "",
|
|
548
|
+
name: defaultVisitor?.name ?? ""
|
|
497
549
|
});
|
|
498
550
|
const transportRef = (0, react.useRef)(null);
|
|
499
551
|
const listRef = (0, react.useRef)(null);
|
|
552
|
+
(0, react.useEffect)(() => {
|
|
553
|
+
if (createTransport) return;
|
|
554
|
+
console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
|
|
555
|
+
}, []);
|
|
500
556
|
const themeStyle = useThemeStyle(session, style);
|
|
501
557
|
const resolvedTheme = useResolvedTheme(theme);
|
|
502
558
|
(0, react.useEffect)(() => () => {
|
|
@@ -539,15 +595,13 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
539
595
|
if (transportRef.current === connection) transportRef.current = null;
|
|
540
596
|
};
|
|
541
597
|
}, [createTransport, session]);
|
|
542
|
-
const
|
|
543
|
-
event.preventDefault();
|
|
598
|
+
const startSession = (0, react.useCallback)(async (identity) => {
|
|
544
599
|
setError(null);
|
|
545
600
|
setIsConnecting(true);
|
|
546
601
|
try {
|
|
547
|
-
const existingVisitorToken = storage?.getItem(createStorageKey(widgetId)) ?? null;
|
|
548
602
|
const nextSession = await getSession({
|
|
549
|
-
visitor,
|
|
550
|
-
visitorToken:
|
|
603
|
+
visitor: identity,
|
|
604
|
+
visitorToken: storage?.getItem(createStorageKey(widgetId)) ?? null,
|
|
551
605
|
widgetId
|
|
552
606
|
});
|
|
553
607
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
@@ -559,7 +613,30 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
559
613
|
} finally {
|
|
560
614
|
setIsConnecting(false);
|
|
561
615
|
}
|
|
616
|
+
}, [
|
|
617
|
+
getSession,
|
|
618
|
+
storage,
|
|
619
|
+
widgetId
|
|
620
|
+
]);
|
|
621
|
+
const submitVisitor = async (event) => {
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
await startSession(visitor);
|
|
562
624
|
};
|
|
625
|
+
(0, react.useEffect)(() => {
|
|
626
|
+
if (!(isOpen && isCompleteVisitor(defaultVisitor))) return;
|
|
627
|
+
if (session || isConnecting || error) return;
|
|
628
|
+
startSession({
|
|
629
|
+
email: defaultVisitor.email.trim(),
|
|
630
|
+
name: defaultVisitor.name.trim()
|
|
631
|
+
}).catch(() => {});
|
|
632
|
+
}, [
|
|
633
|
+
defaultVisitor,
|
|
634
|
+
error,
|
|
635
|
+
isConnecting,
|
|
636
|
+
isOpen,
|
|
637
|
+
session,
|
|
638
|
+
startSession
|
|
639
|
+
]);
|
|
563
640
|
const submitMessage = (event) => {
|
|
564
641
|
event?.preventDefault();
|
|
565
642
|
if (!(session && transportRef.current)) {
|
|
@@ -591,6 +668,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
591
668
|
setError(resolved.message);
|
|
592
669
|
}
|
|
593
670
|
};
|
|
671
|
+
const canSend = Boolean(createTransport) && isReady;
|
|
594
672
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
595
673
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
596
674
|
const sessionSummary = sessionSummaryText(session, greeting);
|
|
@@ -658,7 +736,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
658
736
|
onKeyDown: (event) => {
|
|
659
737
|
if (!isSendKey(event)) return;
|
|
660
738
|
event.preventDefault();
|
|
661
|
-
if (messageDraft.trim() &&
|
|
739
|
+
if (messageDraft.trim() && canSend) submitMessage();
|
|
662
740
|
},
|
|
663
741
|
placeholder: "Write your message",
|
|
664
742
|
value: messageDraft
|
|
@@ -667,10 +745,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
667
745
|
className: "clivly-chat-widget__actions",
|
|
668
746
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
669
747
|
className: "clivly-chat-widget__subtitle",
|
|
670
|
-
children:
|
|
748
|
+
children: composerStatusText(Boolean(createTransport), isReady)
|
|
671
749
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
672
750
|
className: "clivly-chat-widget__button",
|
|
673
|
-
disabled: !(messageDraft.trim() &&
|
|
751
|
+
disabled: !(messageDraft.trim() && canSend),
|
|
674
752
|
type: "submit",
|
|
675
753
|
children: "Send"
|
|
676
754
|
})]
|
|
@@ -732,11 +810,18 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
732
810
|
}),
|
|
733
811
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
734
812
|
className: "clivly-chat-widget__powered",
|
|
735
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
813
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
736
814
|
href: "https://clivly.com",
|
|
737
815
|
rel: "noopener",
|
|
738
816
|
target: "_blank",
|
|
739
|
-
children:
|
|
817
|
+
children: [
|
|
818
|
+
"Powered by",
|
|
819
|
+
" ",
|
|
820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
821
|
+
className: "clivly-chat-widget__powered-mark",
|
|
822
|
+
children: "Clivly"
|
|
823
|
+
})
|
|
824
|
+
]
|
|
740
825
|
})
|
|
741
826
|
})
|
|
742
827
|
]
|
|
@@ -899,6 +984,33 @@ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
|
|
|
899
984
|
//#endregion
|
|
900
985
|
//#region src/browser-mount.tsx
|
|
901
986
|
const MOUNTED_DATA_ATTR = "clivlyMounted";
|
|
987
|
+
/**
|
|
988
|
+
* The identity a script-tag host already knows, read off the tag.
|
|
989
|
+
*
|
|
990
|
+
* The React component takes `defaultVisitor` as a prop; a script tag has only
|
|
991
|
+
* strings, so `data-visitor-name` / `data-visitor-email` are the same seam. A
|
|
992
|
+
* host that server-renders its page can template the signed-in user's details
|
|
993
|
+
* straight into the tag and the visitor never sees the pre-chat form.
|
|
994
|
+
*
|
|
995
|
+
* Returns `undefined` rather than an empty object when neither attribute is
|
|
996
|
+
* present, so the component's own "no identity" path stays untouched. Partial
|
|
997
|
+
* values are passed through deliberately: `ChatWidget` treats one field as a
|
|
998
|
+
* prefill and still renders the form, which is the right outcome for a host
|
|
999
|
+
* that knows an email but not a name.
|
|
1000
|
+
*
|
|
1001
|
+
* This is self-reported identity — anyone can edit the attribute in devtools.
|
|
1002
|
+
* `resolveUser` on the host backend is what makes it trustworthy; this only
|
|
1003
|
+
* removes the typing.
|
|
1004
|
+
*/
|
|
1005
|
+
function readScriptVisitor(dataset) {
|
|
1006
|
+
const email = dataset.visitorEmail?.trim();
|
|
1007
|
+
const name = dataset.visitorName?.trim();
|
|
1008
|
+
if (!(email || name)) return;
|
|
1009
|
+
return {
|
|
1010
|
+
...email ? { email } : {},
|
|
1011
|
+
...name ? { name } : {}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
902
1014
|
function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
|
|
903
1015
|
return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
|
|
904
1016
|
}
|
|
@@ -943,6 +1055,7 @@ function mountChatWidgetScript$1(script = document.currentScript) {
|
|
|
943
1055
|
shadowRoot.append(container);
|
|
944
1056
|
const { root, setTheme } = createWidgetMount({
|
|
945
1057
|
container,
|
|
1058
|
+
defaultVisitor: readScriptVisitor(script.dataset),
|
|
946
1059
|
sessionUrl,
|
|
947
1060
|
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
948
1061
|
theme: parseTheme(script.dataset.theme),
|
package/dist/index.d.cts
CHANGED
|
@@ -24,7 +24,7 @@ interface ResolveThemeOptions {
|
|
|
24
24
|
* document, and guessing dark would flash the wrong palette on hydration for
|
|
25
25
|
* the majority of pages.
|
|
26
26
|
*/
|
|
27
|
-
declare function resolveTheme(theme: ChatWidgetTheme, { root, view }?: ResolveThemeOptions): ResolvedTheme;
|
|
27
|
+
export declare function resolveTheme(theme: ChatWidgetTheme, { root, view }?: ResolveThemeOptions): ResolvedTheme;
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/types.d.ts
|
|
30
30
|
type WidgetSenderType = "visitor" | "operator" | "system";
|
|
@@ -125,18 +125,36 @@ interface CreateWebSocketTransportOptions {
|
|
|
125
125
|
getProtocols?: (session: WidgetSessionPayload) => string | string[] | undefined;
|
|
126
126
|
resolveUrl: (session: WidgetSessionPayload) => string;
|
|
127
127
|
}
|
|
128
|
-
declare function createWebSocketTransport({ createSocket, getProtocols, resolveUrl }: CreateWebSocketTransportOptions): CreateChatTransport;
|
|
128
|
+
export declare function createWebSocketTransport({ createSocket, getProtocols, resolveUrl }: CreateWebSocketTransportOptions): CreateChatTransport;
|
|
129
129
|
interface CreateLoopbackTransportOptions {
|
|
130
130
|
operatorName?: string;
|
|
131
131
|
operatorReply?: (body: string) => string | null;
|
|
132
132
|
replyDelayMs?: number;
|
|
133
133
|
}
|
|
134
|
-
declare function createLoopbackTransport({ operatorName, operatorReply, replyDelayMs }?: CreateLoopbackTransportOptions): CreateChatTransport;
|
|
134
|
+
export declare function createLoopbackTransport({ operatorName, operatorReply, replyDelayMs }?: CreateLoopbackTransportOptions): CreateChatTransport;
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/chat-widget.d.ts
|
|
137
137
|
interface ChatWidgetProps {
|
|
138
138
|
className?: string;
|
|
139
139
|
createTransport?: CreateChatTransport;
|
|
140
|
+
/**
|
|
141
|
+
* Identity the host already knows, used to skip the pre-chat form.
|
|
142
|
+
*
|
|
143
|
+
* When both `name` and `email` are non-empty the widget opens straight into
|
|
144
|
+
* the conversation and bootstraps the session itself, instead of asking a
|
|
145
|
+
* signed-in user to retype what the host app already displays in its own
|
|
146
|
+
* header. A partial value (one field, or blanks) is treated as a prefill:
|
|
147
|
+
* the form still renders, with the known fields filled in.
|
|
148
|
+
*
|
|
149
|
+
* This seeds the *form*, and a form value is self-reported no matter who
|
|
150
|
+
* typed it. It is not, on its own, proof of identity — anyone can pass any
|
|
151
|
+
* address here. Verification is a separate, server-side mechanism: pass
|
|
152
|
+
* `resolveUser` to `createChatSessionHandler` on the host backend and it
|
|
153
|
+
* resolves the caller's real session behind the API key, which is what marks
|
|
154
|
+
* the resulting contact verified. Use both together for a signed-in user:
|
|
155
|
+
* this prop removes the typing, `resolveUser` is what makes it trustworthy.
|
|
156
|
+
*/
|
|
157
|
+
defaultVisitor?: Partial<ChatWidgetVisitor>;
|
|
140
158
|
getSession: GetSession;
|
|
141
159
|
initiallyOpen?: boolean;
|
|
142
160
|
launcherLabel?: string;
|
|
@@ -155,12 +173,12 @@ interface ChatWidgetProps {
|
|
|
155
173
|
theme?: ChatWidgetTheme;
|
|
156
174
|
widgetId: string;
|
|
157
175
|
}
|
|
158
|
-
declare function ChatWidget({ className, createTransport, getSession, initiallyOpen, launcherLabel, storage, style, theme, widgetId }: ChatWidgetProps): ReactElement;
|
|
159
|
-
declare function ChatLauncher(): null;
|
|
160
|
-
declare function ChatPanel(): null;
|
|
161
|
-
declare function MessageComposer(): null;
|
|
162
|
-
declare function PreChatForm(): null;
|
|
163
|
-
declare function ConversationView(): null;
|
|
176
|
+
export declare function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen, launcherLabel, storage, style, theme, widgetId }: ChatWidgetProps): ReactElement;
|
|
177
|
+
export declare function ChatLauncher(): null;
|
|
178
|
+
export declare function ChatPanel(): null;
|
|
179
|
+
export declare function MessageComposer(): null;
|
|
180
|
+
export declare function PreChatForm(): null;
|
|
181
|
+
export declare function ConversationView(): null;
|
|
164
182
|
//#endregion
|
|
165
183
|
//#region src/browser-mount.d.ts
|
|
166
184
|
interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" | "widgetId"> {
|
|
@@ -206,16 +224,16 @@ interface CreateSessionFetcherOptions {
|
|
|
206
224
|
fetchImpl?: typeof fetch;
|
|
207
225
|
sessionUrl: string;
|
|
208
226
|
}
|
|
209
|
-
declare function createSessionFetcher({ fetchImpl, sessionUrl }: CreateSessionFetcherOptions): GetSession;
|
|
227
|
+
export declare function createSessionFetcher({ fetchImpl, sessionUrl }: CreateSessionFetcherOptions): GetSession;
|
|
210
228
|
//#endregion
|
|
211
229
|
//#region src/index.d.ts
|
|
212
|
-
type ChatWidgetMount = ChatWidgetMount$1;
|
|
213
|
-
type MountChatWidgetOptions = MountChatWidgetOptions$1;
|
|
214
|
-
type ScriptMountResult = ScriptMountResult$1;
|
|
215
|
-
declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
|
|
216
|
-
type CreateClivlyTransportOptions = CreateClivlyTransportOptions$1;
|
|
217
|
-
declare const createClivlyTransport: typeof createClivlyTransport$1;
|
|
218
|
-
declare const mountChatWidget: typeof mountChatWidget$1;
|
|
219
|
-
declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
|
|
230
|
+
export type ChatWidgetMount = ChatWidgetMount$1;
|
|
231
|
+
export type MountChatWidgetOptions = MountChatWidgetOptions$1;
|
|
232
|
+
export type ScriptMountResult = ScriptMountResult$1;
|
|
233
|
+
export declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
|
|
234
|
+
export type CreateClivlyTransportOptions = CreateClivlyTransportOptions$1;
|
|
235
|
+
export declare const createClivlyTransport: typeof createClivlyTransport$1;
|
|
236
|
+
export declare const mountChatWidget: typeof mountChatWidget$1;
|
|
237
|
+
export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
|
|
220
238
|
//#endregion
|
|
221
|
-
export {
|
|
239
|
+
export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
|
package/dist/index.d.mts
CHANGED
|
@@ -24,7 +24,7 @@ interface ResolveThemeOptions {
|
|
|
24
24
|
* document, and guessing dark would flash the wrong palette on hydration for
|
|
25
25
|
* the majority of pages.
|
|
26
26
|
*/
|
|
27
|
-
declare function resolveTheme(theme: ChatWidgetTheme, { root, view }?: ResolveThemeOptions): ResolvedTheme;
|
|
27
|
+
export declare function resolveTheme(theme: ChatWidgetTheme, { root, view }?: ResolveThemeOptions): ResolvedTheme;
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/types.d.ts
|
|
30
30
|
type WidgetSenderType = "visitor" | "operator" | "system";
|
|
@@ -125,18 +125,36 @@ interface CreateWebSocketTransportOptions {
|
|
|
125
125
|
getProtocols?: (session: WidgetSessionPayload) => string | string[] | undefined;
|
|
126
126
|
resolveUrl: (session: WidgetSessionPayload) => string;
|
|
127
127
|
}
|
|
128
|
-
declare function createWebSocketTransport({ createSocket, getProtocols, resolveUrl }: CreateWebSocketTransportOptions): CreateChatTransport;
|
|
128
|
+
export declare function createWebSocketTransport({ createSocket, getProtocols, resolveUrl }: CreateWebSocketTransportOptions): CreateChatTransport;
|
|
129
129
|
interface CreateLoopbackTransportOptions {
|
|
130
130
|
operatorName?: string;
|
|
131
131
|
operatorReply?: (body: string) => string | null;
|
|
132
132
|
replyDelayMs?: number;
|
|
133
133
|
}
|
|
134
|
-
declare function createLoopbackTransport({ operatorName, operatorReply, replyDelayMs }?: CreateLoopbackTransportOptions): CreateChatTransport;
|
|
134
|
+
export declare function createLoopbackTransport({ operatorName, operatorReply, replyDelayMs }?: CreateLoopbackTransportOptions): CreateChatTransport;
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/chat-widget.d.ts
|
|
137
137
|
interface ChatWidgetProps {
|
|
138
138
|
className?: string;
|
|
139
139
|
createTransport?: CreateChatTransport;
|
|
140
|
+
/**
|
|
141
|
+
* Identity the host already knows, used to skip the pre-chat form.
|
|
142
|
+
*
|
|
143
|
+
* When both `name` and `email` are non-empty the widget opens straight into
|
|
144
|
+
* the conversation and bootstraps the session itself, instead of asking a
|
|
145
|
+
* signed-in user to retype what the host app already displays in its own
|
|
146
|
+
* header. A partial value (one field, or blanks) is treated as a prefill:
|
|
147
|
+
* the form still renders, with the known fields filled in.
|
|
148
|
+
*
|
|
149
|
+
* This seeds the *form*, and a form value is self-reported no matter who
|
|
150
|
+
* typed it. It is not, on its own, proof of identity — anyone can pass any
|
|
151
|
+
* address here. Verification is a separate, server-side mechanism: pass
|
|
152
|
+
* `resolveUser` to `createChatSessionHandler` on the host backend and it
|
|
153
|
+
* resolves the caller's real session behind the API key, which is what marks
|
|
154
|
+
* the resulting contact verified. Use both together for a signed-in user:
|
|
155
|
+
* this prop removes the typing, `resolveUser` is what makes it trustworthy.
|
|
156
|
+
*/
|
|
157
|
+
defaultVisitor?: Partial<ChatWidgetVisitor>;
|
|
140
158
|
getSession: GetSession;
|
|
141
159
|
initiallyOpen?: boolean;
|
|
142
160
|
launcherLabel?: string;
|
|
@@ -155,12 +173,12 @@ interface ChatWidgetProps {
|
|
|
155
173
|
theme?: ChatWidgetTheme;
|
|
156
174
|
widgetId: string;
|
|
157
175
|
}
|
|
158
|
-
declare function ChatWidget({ className, createTransport, getSession, initiallyOpen, launcherLabel, storage, style, theme, widgetId }: ChatWidgetProps): ReactElement;
|
|
159
|
-
declare function ChatLauncher(): null;
|
|
160
|
-
declare function ChatPanel(): null;
|
|
161
|
-
declare function MessageComposer(): null;
|
|
162
|
-
declare function PreChatForm(): null;
|
|
163
|
-
declare function ConversationView(): null;
|
|
176
|
+
export declare function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen, launcherLabel, storage, style, theme, widgetId }: ChatWidgetProps): ReactElement;
|
|
177
|
+
export declare function ChatLauncher(): null;
|
|
178
|
+
export declare function ChatPanel(): null;
|
|
179
|
+
export declare function MessageComposer(): null;
|
|
180
|
+
export declare function PreChatForm(): null;
|
|
181
|
+
export declare function ConversationView(): null;
|
|
164
182
|
//#endregion
|
|
165
183
|
//#region src/browser-mount.d.ts
|
|
166
184
|
interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" | "widgetId"> {
|
|
@@ -206,16 +224,16 @@ interface CreateSessionFetcherOptions {
|
|
|
206
224
|
fetchImpl?: typeof fetch;
|
|
207
225
|
sessionUrl: string;
|
|
208
226
|
}
|
|
209
|
-
declare function createSessionFetcher({ fetchImpl, sessionUrl }: CreateSessionFetcherOptions): GetSession;
|
|
227
|
+
export declare function createSessionFetcher({ fetchImpl, sessionUrl }: CreateSessionFetcherOptions): GetSession;
|
|
210
228
|
//#endregion
|
|
211
229
|
//#region src/index.d.ts
|
|
212
|
-
type ChatWidgetMount = ChatWidgetMount$1;
|
|
213
|
-
type MountChatWidgetOptions = MountChatWidgetOptions$1;
|
|
214
|
-
type ScriptMountResult = ScriptMountResult$1;
|
|
215
|
-
declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
|
|
216
|
-
type CreateClivlyTransportOptions = CreateClivlyTransportOptions$1;
|
|
217
|
-
declare const createClivlyTransport: typeof createClivlyTransport$1;
|
|
218
|
-
declare const mountChatWidget: typeof mountChatWidget$1;
|
|
219
|
-
declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
|
|
230
|
+
export type ChatWidgetMount = ChatWidgetMount$1;
|
|
231
|
+
export type MountChatWidgetOptions = MountChatWidgetOptions$1;
|
|
232
|
+
export type ScriptMountResult = ScriptMountResult$1;
|
|
233
|
+
export declare const autoMountChatWidgetScript: typeof autoMountChatWidgetScript$1;
|
|
234
|
+
export type CreateClivlyTransportOptions = CreateClivlyTransportOptions$1;
|
|
235
|
+
export declare const createClivlyTransport: typeof createClivlyTransport$1;
|
|
236
|
+
export declare const mountChatWidget: typeof mountChatWidget$1;
|
|
237
|
+
export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
|
|
220
238
|
//#endregion
|
|
221
|
-
export {
|
|
239
|
+
export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
|