@clivly/chat-widget 0.15.0 → 0.16.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 +39 -0
- package/dist/browser-mount.cjs +284 -45
- package/dist/browser-mount.d.cts +31 -0
- package/dist/browser-mount.d.mts +31 -0
- package/dist/browser-mount.mjs +285 -46
- package/dist/browser.cjs +284 -45
- package/dist/browser.d.cts +31 -0
- package/dist/browser.d.mts +31 -0
- package/dist/browser.mjs +285 -46
- package/dist/index.cjs +284 -45
- package/dist/index.d.cts +33 -2
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +285 -46
- package/dist/widget.global.js +101 -9
- package/package.json +1 -1
package/dist/browser.cjs
CHANGED
|
@@ -170,16 +170,95 @@ const WIDGET_STYLES = `
|
|
|
170
170
|
font-size: max(16px, 1em);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
/* Floating placements. The insets keep the launcher off a phone's home
|
|
174
|
+
indicator and rounded corners; --clivly-chat-z-index lets a host slot it
|
|
175
|
+
beneath its own dialogs. */
|
|
176
|
+
.clivly-chat-widget[data-clivly-placement="bottom-right"],
|
|
177
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] {
|
|
178
|
+
bottom: max(1.25rem, env(safe-area-inset-bottom));
|
|
179
|
+
position: fixed;
|
|
180
|
+
z-index: var(--clivly-chat-z-index, 2147483000);
|
|
181
|
+
}
|
|
182
|
+
.clivly-chat-widget[data-clivly-placement="bottom-right"] {
|
|
183
|
+
right: max(1.25rem, env(safe-area-inset-right));
|
|
184
|
+
}
|
|
185
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] {
|
|
186
|
+
left: max(1.25rem, env(safe-area-inset-left));
|
|
187
|
+
}
|
|
173
188
|
.clivly-chat-widget__launcher {
|
|
174
189
|
align-items: center;
|
|
175
190
|
background: var(--clivly-chat-accent);
|
|
176
191
|
border: 0;
|
|
177
192
|
border-radius: 999px;
|
|
193
|
+
box-shadow:
|
|
194
|
+
0 2px 6px rgba(0, 0, 0, 0.18),
|
|
195
|
+
0 10px 28px -10px var(--clivly-chat-accent);
|
|
178
196
|
color: var(--clivly-chat-accent-contrast);
|
|
179
197
|
cursor: pointer;
|
|
180
198
|
display: inline-flex;
|
|
199
|
+
font-weight: 500;
|
|
181
200
|
gap: 0.5rem;
|
|
182
|
-
|
|
201
|
+
justify-content: center;
|
|
202
|
+
min-height: 3rem;
|
|
203
|
+
padding: 0 1.15rem;
|
|
204
|
+
transition: transform 150ms ease-out, filter 150ms ease-out;
|
|
205
|
+
}
|
|
206
|
+
.clivly-chat-widget__launcher:hover {
|
|
207
|
+
filter: brightness(1.06);
|
|
208
|
+
transform: translateY(-1px);
|
|
209
|
+
}
|
|
210
|
+
.clivly-chat-widget__launcher:active {
|
|
211
|
+
transform: scale(0.96);
|
|
212
|
+
}
|
|
213
|
+
.clivly-chat-widget__launcher:focus-visible {
|
|
214
|
+
outline: 2px solid var(--clivly-chat-accent);
|
|
215
|
+
outline-offset: 3px;
|
|
216
|
+
}
|
|
217
|
+
.clivly-chat-widget__launcher-icon {
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
height: 1.25rem;
|
|
220
|
+
width: 1.25rem;
|
|
221
|
+
}
|
|
222
|
+
/* Icon-only: a 48px circle — a comfortable touch target that does not cover
|
|
223
|
+
the host's pagination, toolbars or form controls the way a labelled pill
|
|
224
|
+
does on a phone. The label is clipped, not removed, so it stays the
|
|
225
|
+
button's accessible name. */
|
|
226
|
+
.clivly-chat-widget[data-clivly-launcher="icon"] .clivly-chat-widget__launcher {
|
|
227
|
+
height: 3rem;
|
|
228
|
+
padding: 0;
|
|
229
|
+
width: 3rem;
|
|
230
|
+
}
|
|
231
|
+
.clivly-chat-widget[data-clivly-launcher="icon"] .clivly-chat-widget__launcher-label {
|
|
232
|
+
clip-path: inset(50%);
|
|
233
|
+
height: 1px;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
position: absolute;
|
|
236
|
+
white-space: nowrap;
|
|
237
|
+
width: 1px;
|
|
238
|
+
}
|
|
239
|
+
@media (width < 48rem) {
|
|
240
|
+
.clivly-chat-widget[data-clivly-launcher="auto"] .clivly-chat-widget__launcher {
|
|
241
|
+
height: 3rem;
|
|
242
|
+
padding: 0;
|
|
243
|
+
width: 3rem;
|
|
244
|
+
}
|
|
245
|
+
.clivly-chat-widget[data-clivly-launcher="auto"] .clivly-chat-widget__launcher-label {
|
|
246
|
+
clip-path: inset(50%);
|
|
247
|
+
height: 1px;
|
|
248
|
+
overflow: hidden;
|
|
249
|
+
position: absolute;
|
|
250
|
+
white-space: nowrap;
|
|
251
|
+
width: 1px;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
@media (prefers-reduced-motion: reduce) {
|
|
255
|
+
.clivly-chat-widget__launcher {
|
|
256
|
+
transition: none;
|
|
257
|
+
}
|
|
258
|
+
.clivly-chat-widget__launcher:hover,
|
|
259
|
+
.clivly-chat-widget__launcher:active {
|
|
260
|
+
transform: none;
|
|
261
|
+
}
|
|
183
262
|
}
|
|
184
263
|
.clivly-chat-widget__panel {
|
|
185
264
|
background: var(--clivly-chat-panel);
|
|
@@ -195,6 +274,19 @@ const WIDGET_STYLES = `
|
|
|
195
274
|
bottom: 4.5rem;
|
|
196
275
|
width: min(24rem, calc(100vw - 2rem));
|
|
197
276
|
}
|
|
277
|
+
/* Opens toward the page from whichever corner the launcher is in. */
|
|
278
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] .clivly-chat-widget__panel {
|
|
279
|
+
left: 0;
|
|
280
|
+
right: auto;
|
|
281
|
+
}
|
|
282
|
+
/* dvh: on a phone vh includes the collapsed URL bar, so a 75vh panel's top
|
|
283
|
+
edge could slide under it. The 48px launcher sits lower than the pill. */
|
|
284
|
+
@media (width < 48rem) {
|
|
285
|
+
.clivly-chat-widget__panel {
|
|
286
|
+
bottom: 4rem;
|
|
287
|
+
height: min(44rem, calc(100dvh - 6.5rem));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
198
290
|
.clivly-chat-widget__header {
|
|
199
291
|
border-bottom: 1px solid var(--clivly-chat-border);
|
|
200
292
|
display: flex;
|
|
@@ -562,6 +654,45 @@ function automationBadge(message) {
|
|
|
562
654
|
if (message.senderType !== "system" || !message.automation) return null;
|
|
563
655
|
return AUTOMATION_BADGES[message.automation] ?? "Automated";
|
|
564
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* The button that opens and closes the panel. Its size and shape come from the
|
|
659
|
+
* root's `data-clivly-launcher` (see WIDGET_STYLES); the label is always its
|
|
660
|
+
* accessible name, and `aria-controls` names the panel only while it exists.
|
|
661
|
+
*/
|
|
662
|
+
function Launcher({ label, onToggle, open, panelId }) {
|
|
663
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
664
|
+
"aria-controls": open ? panelId : void 0,
|
|
665
|
+
"aria-expanded": open,
|
|
666
|
+
className: "clivly-chat-widget__launcher",
|
|
667
|
+
onClick: onToggle,
|
|
668
|
+
type: "button",
|
|
669
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LauncherIcon, { open }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
670
|
+
className: "clivly-chat-widget__launcher-label",
|
|
671
|
+
children: label
|
|
672
|
+
})]
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* A chat bubble while closed, a cross while open — so the icon-only launcher
|
|
677
|
+
* still tells a sighted visitor what tapping it will do. Decorative: the
|
|
678
|
+
* label and `aria-expanded` carry the meaning for assistive technology.
|
|
679
|
+
* Inline SVG because the script-tag build renders inside a shadow root with no
|
|
680
|
+
* icon font or stylesheet of the host's to borrow.
|
|
681
|
+
*/
|
|
682
|
+
function LauncherIcon({ open }) {
|
|
683
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
684
|
+
"aria-hidden": "true",
|
|
685
|
+
className: "clivly-chat-widget__launcher-icon",
|
|
686
|
+
fill: "none",
|
|
687
|
+
focusable: "false",
|
|
688
|
+
stroke: "currentColor",
|
|
689
|
+
strokeLinecap: "round",
|
|
690
|
+
strokeLinejoin: "round",
|
|
691
|
+
strokeWidth: 2,
|
|
692
|
+
viewBox: "0 0 24 24",
|
|
693
|
+
children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" })
|
|
694
|
+
});
|
|
695
|
+
}
|
|
565
696
|
function MessageBubble({ message }) {
|
|
566
697
|
const badge = automationBadge(message);
|
|
567
698
|
const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
|
|
@@ -672,9 +803,10 @@ function isCompleteVisitor(visitor) {
|
|
|
672
803
|
* the visitor is told to wait for something that will never arrive. The third
|
|
673
804
|
* string keeps the status honest in all three states.
|
|
674
805
|
*/
|
|
675
|
-
function composerStatusText(hasTransport, isReady) {
|
|
806
|
+
function composerStatusText(hasTransport, isReady, reconnecting) {
|
|
676
807
|
if (!hasTransport) return "Chat is unavailable";
|
|
677
|
-
|
|
808
|
+
if (isReady) return "Conversation ready";
|
|
809
|
+
return reconnecting ? "Reconnecting…" : "Connecting…";
|
|
678
810
|
}
|
|
679
811
|
/**
|
|
680
812
|
* Guided options under the greeting. Native buttons in a labelled group, so
|
|
@@ -740,8 +872,98 @@ function StatusNotice({ assistantThinking, email, showWaitingNotice }) {
|
|
|
740
872
|
children: waitingNoticeText(email)
|
|
741
873
|
});
|
|
742
874
|
}
|
|
743
|
-
|
|
875
|
+
const RECONNECT_BASE_MS = 1e3;
|
|
876
|
+
const RECONNECT_MAX_MS = 3e4;
|
|
877
|
+
/**
|
|
878
|
+
* How long to wait before reconnect attempt `attempt` (0-based): doubling from
|
|
879
|
+
* one second to a 30-second ceiling, ±25% jitter so a fleet of widgets dropped
|
|
880
|
+
* by the same server restart doesn't reconnect in lockstep.
|
|
881
|
+
*/
|
|
882
|
+
function reconnectDelayMs(attempt, random = Math.random) {
|
|
883
|
+
const base = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** attempt);
|
|
884
|
+
return Math.round(base * (.75 + random() * .5));
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Keep the socket for `session` open, and reconnect when it drops — a laptop
|
|
888
|
+
* waking, a network change, a server deploy — instead of leaving the visitor at
|
|
889
|
+
* "Connecting…" until they reload. A reconnect refreshes the session (same
|
|
890
|
+
* conversation, fresh token) and the server's snapshot on connect brings back
|
|
891
|
+
* anything that arrived meanwhile. Returns whether a reconnect is pending.
|
|
892
|
+
*/
|
|
893
|
+
function useLiveConnection({ actions, createTransport, refreshSession, session, setIsReady, transportRef }) {
|
|
894
|
+
const [reconnecting, setReconnecting] = (0, react.useState)(false);
|
|
895
|
+
const attemptRef = (0, react.useRef)(0);
|
|
896
|
+
const refreshRef = (0, react.useRef)(refreshSession);
|
|
897
|
+
const actionsRef = (0, react.useRef)(actions);
|
|
898
|
+
(0, react.useEffect)(() => {
|
|
899
|
+
refreshRef.current = refreshSession;
|
|
900
|
+
actionsRef.current = actions;
|
|
901
|
+
});
|
|
902
|
+
(0, react.useEffect)(() => {
|
|
903
|
+
transportRef.current?.close();
|
|
904
|
+
transportRef.current = null;
|
|
905
|
+
setIsReady(false);
|
|
906
|
+
if (!(session && createTransport)) return;
|
|
907
|
+
let disposed = false;
|
|
908
|
+
let timer;
|
|
909
|
+
const retry = () => {
|
|
910
|
+
timer = void 0;
|
|
911
|
+
refreshRef.current(session).catch(() => {
|
|
912
|
+
scheduleReconnect();
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
const scheduleReconnect = () => {
|
|
916
|
+
if (disposed || timer !== void 0) return;
|
|
917
|
+
setReconnecting(true);
|
|
918
|
+
timer = setTimeout(retry, reconnectDelayMs(attemptRef.current));
|
|
919
|
+
attemptRef.current += 1;
|
|
920
|
+
};
|
|
921
|
+
const retryNow = () => {
|
|
922
|
+
if (timer !== void 0) {
|
|
923
|
+
clearTimeout(timer);
|
|
924
|
+
retry();
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
window.addEventListener("online", retryNow);
|
|
928
|
+
const connection = createTransport({
|
|
929
|
+
handlers: {
|
|
930
|
+
onClose: () => {
|
|
931
|
+
setIsReady(false);
|
|
932
|
+
actionsRef.current.setAssistantThinking(false);
|
|
933
|
+
scheduleReconnect();
|
|
934
|
+
},
|
|
935
|
+
onError: (transportError) => {
|
|
936
|
+
if (attemptRef.current === 0) actionsRef.current.setError(transportError.message);
|
|
937
|
+
},
|
|
938
|
+
onEvent: (event) => {
|
|
939
|
+
applyServerEvent(event, {
|
|
940
|
+
...actionsRef.current,
|
|
941
|
+
setIsReady
|
|
942
|
+
});
|
|
943
|
+
},
|
|
944
|
+
onOpen: () => {
|
|
945
|
+
if (attemptRef.current > 0) actionsRef.current.setError(null);
|
|
946
|
+
attemptRef.current = 0;
|
|
947
|
+
setReconnecting(false);
|
|
948
|
+
setIsReady(true);
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
session
|
|
952
|
+
});
|
|
953
|
+
transportRef.current = connection;
|
|
954
|
+
return () => {
|
|
955
|
+
disposed = true;
|
|
956
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
957
|
+
window.removeEventListener("online", retryNow);
|
|
958
|
+
connection.close();
|
|
959
|
+
if (transportRef.current === connection) transportRef.current = null;
|
|
960
|
+
};
|
|
961
|
+
}, [createTransport, session]);
|
|
962
|
+
return reconnecting;
|
|
963
|
+
}
|
|
964
|
+
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcher = "auto", launcherLabel = "Chat with us", placement = "bottom-right", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
744
965
|
const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
|
|
966
|
+
const panelId = (0, react.useId)();
|
|
745
967
|
const [error, setError] = (0, react.useState)(null);
|
|
746
968
|
const [isConnecting, setIsConnecting] = (0, react.useState)(false);
|
|
747
969
|
const [isReady, setIsReady] = (0, react.useState)(false);
|
|
@@ -756,6 +978,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
756
978
|
});
|
|
757
979
|
const transportRef = (0, react.useRef)(null);
|
|
758
980
|
const listRef = (0, react.useRef)(null);
|
|
981
|
+
const identityRef = (0, react.useRef)(null);
|
|
759
982
|
(0, react.useEffect)(() => {
|
|
760
983
|
if (createTransport) return;
|
|
761
984
|
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");
|
|
@@ -771,41 +994,30 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
771
994
|
if (!listRef.current) return;
|
|
772
995
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
773
996
|
}, [messages]);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
setIsReady(true);
|
|
799
|
-
}
|
|
800
|
-
},
|
|
801
|
-
session
|
|
802
|
-
});
|
|
803
|
-
transportRef.current = connection;
|
|
804
|
-
return () => {
|
|
805
|
-
connection.close();
|
|
806
|
-
if (transportRef.current === connection) transportRef.current = null;
|
|
807
|
-
};
|
|
808
|
-
}, [createTransport, session]);
|
|
997
|
+
const reconnecting = useLiveConnection({
|
|
998
|
+
actions: {
|
|
999
|
+
setAssistantThinking,
|
|
1000
|
+
setError,
|
|
1001
|
+
setMessages,
|
|
1002
|
+
setSupportState
|
|
1003
|
+
},
|
|
1004
|
+
createTransport,
|
|
1005
|
+
refreshSession: async (current) => {
|
|
1006
|
+
const identity = identityRef.current;
|
|
1007
|
+
if (!identity) throw new Error("No visitor to reconnect as.");
|
|
1008
|
+
const next = await getSession({
|
|
1009
|
+
visitor: identity,
|
|
1010
|
+
visitorToken: current.visitorToken,
|
|
1011
|
+
widgetId
|
|
1012
|
+
});
|
|
1013
|
+
storage?.setItem(createStorageKey(widgetId), next.visitorToken);
|
|
1014
|
+
setSupportState(next.support?.state ?? null);
|
|
1015
|
+
setSession(next);
|
|
1016
|
+
},
|
|
1017
|
+
session,
|
|
1018
|
+
setIsReady,
|
|
1019
|
+
transportRef
|
|
1020
|
+
});
|
|
809
1021
|
(0, react.useEffect)(() => {
|
|
810
1022
|
if (!(isOpen && isReady)) return;
|
|
811
1023
|
const markReadIfVisible = () => {
|
|
@@ -833,6 +1045,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
833
1045
|
widgetId
|
|
834
1046
|
});
|
|
835
1047
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
1048
|
+
identityRef.current = identity;
|
|
836
1049
|
setSupportState(nextSession.support?.state ?? null);
|
|
837
1050
|
setSession(nextSession);
|
|
838
1051
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
@@ -910,21 +1123,24 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
910
1123
|
const topics = visibleTopics(session, supportState);
|
|
911
1124
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
912
1125
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
1126
|
+
"data-clivly-launcher": launcher,
|
|
1127
|
+
"data-clivly-placement": placement,
|
|
913
1128
|
"data-clivly-theme": resolvedTheme,
|
|
914
1129
|
style: themeStyle,
|
|
915
1130
|
children: [
|
|
916
1131
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDGET_STYLES }),
|
|
917
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
918
|
-
|
|
919
|
-
|
|
1132
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Launcher, {
|
|
1133
|
+
label: launcherLabel,
|
|
1134
|
+
onToggle: () => {
|
|
920
1135
|
setIsOpen((open) => !open);
|
|
921
1136
|
},
|
|
922
|
-
|
|
923
|
-
|
|
1137
|
+
open: isOpen,
|
|
1138
|
+
panelId
|
|
924
1139
|
}),
|
|
925
1140
|
isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
926
1141
|
"aria-label": panelTitle,
|
|
927
1142
|
className: "clivly-chat-widget__panel",
|
|
1143
|
+
id: panelId,
|
|
928
1144
|
children: [
|
|
929
1145
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
930
1146
|
className: "clivly-chat-widget__header",
|
|
@@ -997,8 +1213,9 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
997
1213
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
998
1214
|
className: "clivly-chat-widget__actions",
|
|
999
1215
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1216
|
+
"aria-live": "polite",
|
|
1000
1217
|
className: "clivly-chat-widget__subtitle",
|
|
1001
|
-
children: composerStatusText(Boolean(createTransport), isReady)
|
|
1218
|
+
children: composerStatusText(Boolean(createTransport), isReady, reconnecting)
|
|
1002
1219
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1003
1220
|
className: "clivly-chat-widget__button",
|
|
1004
1221
|
disabled: !(messageDraft.trim() && canSend),
|
|
@@ -1147,6 +1364,26 @@ function createClivlyTransport(options = {}) {
|
|
|
1147
1364
|
});
|
|
1148
1365
|
}
|
|
1149
1366
|
//#endregion
|
|
1367
|
+
//#region src/layout.ts
|
|
1368
|
+
const PLACEMENTS = [
|
|
1369
|
+
"bottom-left",
|
|
1370
|
+
"bottom-right",
|
|
1371
|
+
"inline"
|
|
1372
|
+
];
|
|
1373
|
+
const LAUNCHERS = [
|
|
1374
|
+
"auto",
|
|
1375
|
+
"icon",
|
|
1376
|
+
"pill"
|
|
1377
|
+
];
|
|
1378
|
+
/** A script tag's `data-placement`; anything unrecognised floats bottom-right. */
|
|
1379
|
+
function parsePlacement(value) {
|
|
1380
|
+
return PLACEMENTS.find((placement) => placement === value) ?? "bottom-right";
|
|
1381
|
+
}
|
|
1382
|
+
/** A script tag's `data-launcher`; anything unrecognised is `"auto"`. */
|
|
1383
|
+
function parseLauncher(value) {
|
|
1384
|
+
return LAUNCHERS.find((launcher) => launcher === value) ?? "auto";
|
|
1385
|
+
}
|
|
1386
|
+
//#endregion
|
|
1150
1387
|
//#region src/session.ts
|
|
1151
1388
|
function isWidgetSessionPayload(value) {
|
|
1152
1389
|
return typeof value === "object" && value !== null;
|
|
@@ -1244,6 +1481,8 @@ function mountChatWidgetScript$1(script = document.currentScript) {
|
|
|
1244
1481
|
const { root, setTheme } = createWidgetMount({
|
|
1245
1482
|
container,
|
|
1246
1483
|
defaultVisitor: readScriptVisitor(script.dataset),
|
|
1484
|
+
launcher: parseLauncher(script.dataset.launcher),
|
|
1485
|
+
placement: parsePlacement(script.dataset.placement),
|
|
1247
1486
|
sessionUrl,
|
|
1248
1487
|
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
1249
1488
|
theme: parseTheme(script.dataset.theme),
|
package/dist/browser.d.cts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { Root } from "react-dom/client";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
|
+
//#region src/layout.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where the widget sits on the host page, and what its launcher looks like.
|
|
6
|
+
*
|
|
7
|
+
* Until 0.16 the widget rendered in normal flow wherever it was mounted — the
|
|
8
|
+
* script tag inserted it directly after itself, so most embeds showed a
|
|
9
|
+
* full-width-label pill at the end of the page rather than a floating button.
|
|
10
|
+
* Floating in a corner is now the default; `"inline"` keeps the old behaviour
|
|
11
|
+
* for a host that positions the widget itself.
|
|
12
|
+
*/
|
|
13
|
+
type ChatWidgetPlacement = "bottom-left" | "bottom-right" | "inline";
|
|
14
|
+
/**
|
|
15
|
+
* - `"auto"` (default): the labelled pill on wide screens, a 48px icon-only
|
|
16
|
+
* button below 48rem, where a pill covers the host's own controls.
|
|
17
|
+
* - `"pill"`: always the labelled pill.
|
|
18
|
+
* - `"icon"`: always the icon-only button.
|
|
19
|
+
*
|
|
20
|
+
* The label stays the button's accessible name in every variant.
|
|
21
|
+
*/
|
|
22
|
+
type ChatWidgetLauncher = "auto" | "icon" | "pill";
|
|
23
|
+
//#endregion
|
|
3
24
|
//#region src/theme.d.ts
|
|
4
25
|
/**
|
|
5
26
|
* Theme resolution for the widget.
|
|
@@ -177,7 +198,17 @@ interface ChatWidgetProps {
|
|
|
177
198
|
defaultVisitor?: Partial<ChatWidgetVisitor>;
|
|
178
199
|
getSession: GetSession;
|
|
179
200
|
initiallyOpen?: boolean;
|
|
201
|
+
/** Launcher shape; see {@link ChatWidgetLauncher}. Defaults to `"auto"`. */
|
|
202
|
+
launcher?: ChatWidgetLauncher;
|
|
203
|
+
/** The launcher's visible text, and always its accessible name. */
|
|
180
204
|
launcherLabel?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Where the widget sits; see {@link ChatWidgetPlacement}. Defaults to
|
|
207
|
+
* `"bottom-right"`, floating clear of the device's safe-area insets. Stacks
|
|
208
|
+
* at `--clivly-chat-z-index` (default 2147483000) — lower it through
|
|
209
|
+
* `style` to sit beneath the host's own dialogs.
|
|
210
|
+
*/
|
|
211
|
+
placement?: ChatWidgetPlacement;
|
|
181
212
|
storage?: Pick<Storage, "getItem" | "setItem">;
|
|
182
213
|
/**
|
|
183
214
|
* Inline styles on the widget root. The intended use is overriding palette
|
package/dist/browser.d.mts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { Root } from "react-dom/client";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
|
+
//#region src/layout.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where the widget sits on the host page, and what its launcher looks like.
|
|
6
|
+
*
|
|
7
|
+
* Until 0.16 the widget rendered in normal flow wherever it was mounted — the
|
|
8
|
+
* script tag inserted it directly after itself, so most embeds showed a
|
|
9
|
+
* full-width-label pill at the end of the page rather than a floating button.
|
|
10
|
+
* Floating in a corner is now the default; `"inline"` keeps the old behaviour
|
|
11
|
+
* for a host that positions the widget itself.
|
|
12
|
+
*/
|
|
13
|
+
type ChatWidgetPlacement = "bottom-left" | "bottom-right" | "inline";
|
|
14
|
+
/**
|
|
15
|
+
* - `"auto"` (default): the labelled pill on wide screens, a 48px icon-only
|
|
16
|
+
* button below 48rem, where a pill covers the host's own controls.
|
|
17
|
+
* - `"pill"`: always the labelled pill.
|
|
18
|
+
* - `"icon"`: always the icon-only button.
|
|
19
|
+
*
|
|
20
|
+
* The label stays the button's accessible name in every variant.
|
|
21
|
+
*/
|
|
22
|
+
type ChatWidgetLauncher = "auto" | "icon" | "pill";
|
|
23
|
+
//#endregion
|
|
3
24
|
//#region src/theme.d.ts
|
|
4
25
|
/**
|
|
5
26
|
* Theme resolution for the widget.
|
|
@@ -177,7 +198,17 @@ interface ChatWidgetProps {
|
|
|
177
198
|
defaultVisitor?: Partial<ChatWidgetVisitor>;
|
|
178
199
|
getSession: GetSession;
|
|
179
200
|
initiallyOpen?: boolean;
|
|
201
|
+
/** Launcher shape; see {@link ChatWidgetLauncher}. Defaults to `"auto"`. */
|
|
202
|
+
launcher?: ChatWidgetLauncher;
|
|
203
|
+
/** The launcher's visible text, and always its accessible name. */
|
|
180
204
|
launcherLabel?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Where the widget sits; see {@link ChatWidgetPlacement}. Defaults to
|
|
207
|
+
* `"bottom-right"`, floating clear of the device's safe-area insets. Stacks
|
|
208
|
+
* at `--clivly-chat-z-index` (default 2147483000) — lower it through
|
|
209
|
+
* `style` to sit beneath the host's own dialogs.
|
|
210
|
+
*/
|
|
211
|
+
placement?: ChatWidgetPlacement;
|
|
181
212
|
storage?: Pick<Storage, "getItem" | "setItem">;
|
|
182
213
|
/**
|
|
183
214
|
* Inline styles on the widget root. The intended use is overriding palette
|