@clivly/chat-widget 0.16.0 → 0.17.1
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/LICENSE +21 -0
- package/README.md +30 -13
- package/dist/browser-mount.cjs +528 -92
- package/dist/browser-mount.d.cts +22 -13
- package/dist/browser-mount.d.mts +22 -13
- package/dist/browser-mount.mjs +528 -92
- package/dist/browser.cjs +529 -94
- package/dist/browser.d.cts +22 -15
- package/dist/browser.d.mts +22 -15
- package/dist/browser.mjs +529 -94
- package/dist/index.cjs +529 -94
- package/dist/index.d.cts +21 -19
- package/dist/index.d.mts +21 -19
- package/dist/index.mjs +529 -94
- package/dist/widget.global.js +179 -20
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRoot } from "react-dom/client";
|
|
2
2
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
|
|
4
|
+
// src/theme.ts
|
|
5
5
|
const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
6
6
|
/** The attribute set by next-themes, daisyUI, and most `data-*` theme setups. */
|
|
7
7
|
const THEME_ATTRIBUTE = "data-theme";
|
|
@@ -109,8 +109,88 @@ function parseTheme(value) {
|
|
|
109
109
|
if (value === "auto" || value === "dark" || value === "light") return value;
|
|
110
110
|
return "auto";
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// src/visitor-token-storage.ts
|
|
113
|
+
/** Normalised the way the server matches contacts: trimmed, lower-case. */
|
|
114
|
+
function normalizeVisitorEmail(email) {
|
|
115
|
+
return email.trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
const HASH_MODULUS = 4294967291;
|
|
118
|
+
/**
|
|
119
|
+
* A polynomial string hash, reduced mod a prime below 2^32: stable,
|
|
120
|
+
* synchronous, and exact in doubles (every intermediate stays under 2^53).
|
|
121
|
+
* Not a security primitive.
|
|
122
|
+
*/
|
|
123
|
+
function shortHash(value) {
|
|
124
|
+
let hash = 5381;
|
|
125
|
+
for (const char of value) hash = (hash * 33 + (char.codePointAt(0) ?? 0)) % HASH_MODULUS;
|
|
126
|
+
return hash.toString(16).padStart(8, "0");
|
|
127
|
+
}
|
|
128
|
+
function visitorTokenKey(widgetId, email) {
|
|
129
|
+
return `clivly-chat-widget:${widgetId}:visitor-token:${shortHash(normalizeVisitorEmail(email))}`;
|
|
130
|
+
}
|
|
131
|
+
/** The single per-widget key every widget before this change wrote. */
|
|
132
|
+
function legacyVisitorTokenKey(widgetId) {
|
|
133
|
+
return `clivly-chat-widget:${widgetId}:visitor-token`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The token to offer for this person. Falls back to the legacy per-widget
|
|
137
|
+
* token once, so visitors who chatted before this change still resume — the
|
|
138
|
+
* server only honours it if they are the identity that thread belongs to.
|
|
139
|
+
*/
|
|
140
|
+
function readVisitorToken(storage, widgetId, email) {
|
|
141
|
+
if (!storage) return null;
|
|
142
|
+
try {
|
|
143
|
+
return storage.getItem(visitorTokenKey(widgetId, email)) ?? storage.getItem(legacyVisitorTokenKey(widgetId));
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Store this person's token and retire the legacy shared one. */
|
|
149
|
+
function writeVisitorToken(storage, widgetId, email, token) {
|
|
150
|
+
if (!storage) return;
|
|
151
|
+
try {
|
|
152
|
+
storage.setItem(visitorTokenKey(widgetId, email), token);
|
|
153
|
+
storage.removeItem?.(legacyVisitorTokenKey(widgetId));
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
// src/widget-presentation-cache.ts
|
|
157
|
+
function presentationKey(widgetId) {
|
|
158
|
+
return `clivly-chat-widget:${widgetId}:presentation`;
|
|
159
|
+
}
|
|
160
|
+
function isPresentation(value) {
|
|
161
|
+
if (typeof value !== "object" || value === null) return false;
|
|
162
|
+
const candidate = value;
|
|
163
|
+
return typeof candidate.name === "string" && typeof candidate.config === "object" && candidate.config !== null && Array.isArray(candidate.topics);
|
|
164
|
+
}
|
|
165
|
+
function readPresentation(storage, widgetId) {
|
|
166
|
+
if (!storage) return null;
|
|
167
|
+
try {
|
|
168
|
+
const raw = storage.getItem(presentationKey(widgetId));
|
|
169
|
+
if (!raw) return null;
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
return isPresentation(parsed) ? parsed : null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function presentationOf(session) {
|
|
177
|
+
return {
|
|
178
|
+
config: {
|
|
179
|
+
brandColor: session.widget.config.brandColor ?? null,
|
|
180
|
+
greeting: session.widget.config.greeting ?? null
|
|
181
|
+
},
|
|
182
|
+
name: session.widget.name,
|
|
183
|
+
supportState: session.support?.state ?? null,
|
|
184
|
+
topics: session.support?.topics ?? []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function writePresentation(storage, widgetId, presentation) {
|
|
188
|
+
if (!storage) return;
|
|
189
|
+
try {
|
|
190
|
+
storage.setItem(presentationKey(widgetId), JSON.stringify(presentation));
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
// src/chat-widget.tsx
|
|
114
194
|
const DELIVERY_LABELS = {
|
|
115
195
|
failed: "Failed to send",
|
|
116
196
|
pending: "Sending…",
|
|
@@ -264,9 +344,9 @@ const WIDGET_STYLES = `
|
|
|
264
344
|
border: 1px solid var(--clivly-chat-border);
|
|
265
345
|
border-radius: 1rem;
|
|
266
346
|
box-shadow: 0 24px 80px var(--clivly-chat-shadow);
|
|
267
|
-
display:
|
|
268
|
-
|
|
269
|
-
height: min(
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-direction: column;
|
|
349
|
+
height: min(40rem, 75vh);
|
|
270
350
|
overflow: hidden;
|
|
271
351
|
position: absolute;
|
|
272
352
|
right: 0;
|
|
@@ -283,20 +363,56 @@ const WIDGET_STYLES = `
|
|
|
283
363
|
@media (width < 48rem) {
|
|
284
364
|
.clivly-chat-widget__panel {
|
|
285
365
|
bottom: 4rem;
|
|
286
|
-
height: min(
|
|
366
|
+
height: min(40rem, calc(100dvh - 6.5rem));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/* On a phone a floating card leaves a strip of page above it and puts the
|
|
370
|
+
launcher over its own composer, so the panel becomes a full-screen sheet
|
|
371
|
+
instead — the close button in its header takes over from the launcher,
|
|
372
|
+
which is hidden while it is open. */
|
|
373
|
+
@media (width < 30rem) {
|
|
374
|
+
.clivly-chat-widget__panel {
|
|
375
|
+
border: 0;
|
|
376
|
+
border-radius: 0;
|
|
377
|
+
bottom: 0;
|
|
378
|
+
height: 100dvh;
|
|
379
|
+
left: 0;
|
|
380
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
381
|
+
padding-top: env(safe-area-inset-top);
|
|
382
|
+
position: fixed;
|
|
383
|
+
right: 0;
|
|
384
|
+
top: 0;
|
|
385
|
+
width: 100vw;
|
|
386
|
+
}
|
|
387
|
+
.clivly-chat-widget[data-clivly-open] .clivly-chat-widget__launcher {
|
|
388
|
+
display: none;
|
|
287
389
|
}
|
|
288
390
|
}
|
|
289
391
|
.clivly-chat-widget__header {
|
|
392
|
+
align-items: center;
|
|
290
393
|
border-bottom: 1px solid var(--clivly-chat-border);
|
|
291
394
|
display: flex;
|
|
395
|
+
flex: 0 0 auto;
|
|
292
396
|
justify-content: space-between;
|
|
293
|
-
gap:
|
|
294
|
-
padding: 1rem;
|
|
397
|
+
gap: 0.75rem;
|
|
398
|
+
padding: 0.75rem 0.75rem 0.75rem 1rem;
|
|
399
|
+
}
|
|
400
|
+
.clivly-chat-widget__heading {
|
|
401
|
+
min-width: 0;
|
|
295
402
|
}
|
|
296
403
|
.clivly-chat-widget__title {
|
|
297
404
|
font-size: 1rem;
|
|
298
405
|
font-weight: 600;
|
|
299
406
|
margin: 0;
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
text-overflow: ellipsis;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
}
|
|
411
|
+
.clivly-chat-widget__header .clivly-chat-widget__subtitle {
|
|
412
|
+
margin-top: 0.125rem;
|
|
413
|
+
overflow: hidden;
|
|
414
|
+
text-overflow: ellipsis;
|
|
415
|
+
white-space: nowrap;
|
|
300
416
|
}
|
|
301
417
|
.clivly-chat-widget__subtitle {
|
|
302
418
|
color: var(--clivly-chat-muted);
|
|
@@ -304,19 +420,50 @@ const WIDGET_STYLES = `
|
|
|
304
420
|
margin: 0.25rem 0 0;
|
|
305
421
|
}
|
|
306
422
|
.clivly-chat-widget__close {
|
|
423
|
+
align-items: center;
|
|
307
424
|
background: transparent;
|
|
308
425
|
border: 0;
|
|
426
|
+
border-radius: 999px;
|
|
309
427
|
color: var(--clivly-chat-muted);
|
|
310
428
|
cursor: pointer;
|
|
429
|
+
display: inline-flex;
|
|
430
|
+
flex: 0 0 auto;
|
|
431
|
+
height: 2.25rem;
|
|
432
|
+
justify-content: center;
|
|
311
433
|
padding: 0;
|
|
434
|
+
width: 2.25rem;
|
|
435
|
+
}
|
|
436
|
+
.clivly-chat-widget__close:hover,
|
|
437
|
+
.clivly-chat-widget__close:focus-visible {
|
|
438
|
+
background: var(--clivly-chat-operator);
|
|
439
|
+
color: var(--clivly-chat-text);
|
|
440
|
+
}
|
|
441
|
+
.clivly-chat-widget__close:focus-visible {
|
|
442
|
+
outline: 2px solid var(--clivly-chat-accent);
|
|
443
|
+
outline-offset: 2px;
|
|
444
|
+
}
|
|
445
|
+
.clivly-chat-widget__close svg {
|
|
446
|
+
height: 1.25rem;
|
|
447
|
+
width: 1.25rem;
|
|
312
448
|
}
|
|
313
449
|
.clivly-chat-widget__body {
|
|
314
450
|
display: flex;
|
|
451
|
+
flex: 1 1 auto;
|
|
315
452
|
flex-direction: column;
|
|
316
453
|
gap: 0.75rem;
|
|
454
|
+
min-height: 0;
|
|
317
455
|
overflow-y: auto;
|
|
456
|
+
overscroll-behavior: contain;
|
|
318
457
|
padding: 1rem;
|
|
319
458
|
}
|
|
459
|
+
.clivly-chat-widget__visually-hidden {
|
|
460
|
+
clip-path: inset(50%);
|
|
461
|
+
height: 1px;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
position: absolute;
|
|
464
|
+
white-space: nowrap;
|
|
465
|
+
width: 1px;
|
|
466
|
+
}
|
|
320
467
|
.clivly-chat-widget__message {
|
|
321
468
|
border-radius: 1rem;
|
|
322
469
|
max-width: 85%;
|
|
@@ -394,6 +541,11 @@ const WIDGET_STYLES = `
|
|
|
394
541
|
.clivly-chat-widget__powered a:focus-visible {
|
|
395
542
|
text-decoration: underline;
|
|
396
543
|
}
|
|
544
|
+
.clivly-chat-widget__powered,
|
|
545
|
+
.clivly-chat-widget__composer,
|
|
546
|
+
.clivly-chat-widget__form {
|
|
547
|
+
flex: 0 0 auto;
|
|
548
|
+
}
|
|
397
549
|
.clivly-chat-widget__composer,
|
|
398
550
|
.clivly-chat-widget__form {
|
|
399
551
|
border-top: 1px solid var(--clivly-chat-border);
|
|
@@ -401,6 +553,62 @@ const WIDGET_STYLES = `
|
|
|
401
553
|
gap: 0.75rem;
|
|
402
554
|
padding: 1rem;
|
|
403
555
|
}
|
|
556
|
+
.clivly-chat-widget__composer {
|
|
557
|
+
gap: 0.375rem;
|
|
558
|
+
padding: 0.75rem;
|
|
559
|
+
}
|
|
560
|
+
/* With topics docked above it, the composer has no border of its own: the
|
|
561
|
+
strip and the field read as one input area. */
|
|
562
|
+
.clivly-chat-widget__topics + .clivly-chat-widget__composer {
|
|
563
|
+
border-top: 0;
|
|
564
|
+
}
|
|
565
|
+
.clivly-chat-widget__composer-row {
|
|
566
|
+
align-items: flex-end;
|
|
567
|
+
display: flex;
|
|
568
|
+
gap: 0.5rem;
|
|
569
|
+
}
|
|
570
|
+
.clivly-chat-widget__composer-field {
|
|
571
|
+
flex: 1 1 auto;
|
|
572
|
+
min-width: 0;
|
|
573
|
+
}
|
|
574
|
+
.clivly-chat-widget__composer .clivly-chat-widget__textarea {
|
|
575
|
+
line-height: 1.4;
|
|
576
|
+
max-height: 8rem;
|
|
577
|
+
min-height: 2.75rem;
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
padding: 0.625rem 0.75rem;
|
|
580
|
+
resize: none;
|
|
581
|
+
}
|
|
582
|
+
.clivly-chat-widget__send {
|
|
583
|
+
align-items: center;
|
|
584
|
+
border-radius: 999px;
|
|
585
|
+
display: inline-flex;
|
|
586
|
+
height: 2.75rem;
|
|
587
|
+
justify-content: center;
|
|
588
|
+
padding: 0;
|
|
589
|
+
width: 2.75rem;
|
|
590
|
+
}
|
|
591
|
+
.clivly-chat-widget__send svg {
|
|
592
|
+
height: 1.25rem;
|
|
593
|
+
width: 1.25rem;
|
|
594
|
+
}
|
|
595
|
+
.clivly-chat-widget__composer .clivly-chat-widget__actions {
|
|
596
|
+
padding: 0 0.25rem;
|
|
597
|
+
}
|
|
598
|
+
.clivly-chat-widget__composer .clivly-chat-widget__subtitle {
|
|
599
|
+
font-size: 0.75rem;
|
|
600
|
+
margin: 0;
|
|
601
|
+
}
|
|
602
|
+
/* Once the conversation is ready the status has nothing to say to a sighted
|
|
603
|
+
visitor; it stays in the accessibility tree so the change is announced. */
|
|
604
|
+
.clivly-chat-widget__actions[data-ready] {
|
|
605
|
+
clip-path: inset(50%);
|
|
606
|
+
height: 1px;
|
|
607
|
+
overflow: hidden;
|
|
608
|
+
position: absolute;
|
|
609
|
+
white-space: nowrap;
|
|
610
|
+
width: 1px;
|
|
611
|
+
}
|
|
404
612
|
.clivly-chat-widget__field {
|
|
405
613
|
display: grid;
|
|
406
614
|
gap: 0.35rem;
|
|
@@ -481,13 +689,42 @@ const WIDGET_STYLES = `
|
|
|
481
689
|
font-size: 0.6875rem;
|
|
482
690
|
padding: 0 0.4rem;
|
|
483
691
|
}
|
|
692
|
+
/* Docked above the composer. Wide screens wrap up to about two rows and
|
|
693
|
+
scroll beyond that; narrow or touch screens get one sideways-scrolling row
|
|
694
|
+
whose right edge fades to show there is more. */
|
|
484
695
|
.clivly-chat-widget__topics {
|
|
696
|
+
border-top: 1px solid var(--clivly-chat-border);
|
|
485
697
|
display: flex;
|
|
698
|
+
flex: 0 0 auto;
|
|
486
699
|
flex-wrap: wrap;
|
|
487
|
-
gap: 0.
|
|
488
|
-
margin: 0;
|
|
489
|
-
padding: 0;
|
|
700
|
+
gap: 0.375rem;
|
|
490
701
|
list-style: none;
|
|
702
|
+
margin: 0;
|
|
703
|
+
max-height: 5.25rem;
|
|
704
|
+
overflow-y: auto;
|
|
705
|
+
padding: 0.625rem 0.75rem 0;
|
|
706
|
+
scrollbar-width: thin;
|
|
707
|
+
}
|
|
708
|
+
.clivly-chat-widget__topics > li {
|
|
709
|
+
flex: 0 0 auto;
|
|
710
|
+
scroll-snap-align: start;
|
|
711
|
+
}
|
|
712
|
+
@media (width < 48rem), (any-pointer: coarse) {
|
|
713
|
+
.clivly-chat-widget__topics {
|
|
714
|
+
flex-wrap: nowrap;
|
|
715
|
+
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
|
|
716
|
+
mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
|
|
717
|
+
max-height: none;
|
|
718
|
+
overflow-x: auto;
|
|
719
|
+
overflow-y: hidden;
|
|
720
|
+
padding-right: 1.5rem;
|
|
721
|
+
scroll-padding-left: 0.75rem;
|
|
722
|
+
scroll-snap-type: x proximity;
|
|
723
|
+
scrollbar-width: none;
|
|
724
|
+
}
|
|
725
|
+
.clivly-chat-widget__topics::-webkit-scrollbar {
|
|
726
|
+
display: none;
|
|
727
|
+
}
|
|
491
728
|
}
|
|
492
729
|
.clivly-chat-widget__topic {
|
|
493
730
|
background: var(--clivly-chat-panel);
|
|
@@ -495,9 +732,11 @@ const WIDGET_STYLES = `
|
|
|
495
732
|
border-radius: 999px;
|
|
496
733
|
color: var(--clivly-chat-text);
|
|
497
734
|
cursor: pointer;
|
|
498
|
-
font-size: 0.
|
|
499
|
-
|
|
500
|
-
|
|
735
|
+
font-size: 0.8125rem;
|
|
736
|
+
line-height: 1.2;
|
|
737
|
+
min-height: 2rem;
|
|
738
|
+
padding: 0.375rem 0.75rem;
|
|
739
|
+
white-space: nowrap;
|
|
501
740
|
}
|
|
502
741
|
.clivly-chat-widget__topic:hover,
|
|
503
742
|
.clivly-chat-widget__topic:focus-visible {
|
|
@@ -532,9 +771,6 @@ function createLocalMessage(message, delivery = "sent") {
|
|
|
532
771
|
delivery
|
|
533
772
|
};
|
|
534
773
|
}
|
|
535
|
-
function createStorageKey(widgetId) {
|
|
536
|
-
return `clivly-chat-widget:${widgetId}:visitor-token`;
|
|
537
|
-
}
|
|
538
774
|
function formatTimestamp(value) {
|
|
539
775
|
const date = new Date(value);
|
|
540
776
|
if (Number.isNaN(date.getTime())) return "";
|
|
@@ -576,15 +812,44 @@ function markMessageFailed(messages, clientMessageId) {
|
|
|
576
812
|
function sortMessages(messages) {
|
|
577
813
|
return [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
578
814
|
}
|
|
579
|
-
|
|
815
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
816
|
+
/** Relative luminance above which white text stops being readable. */
|
|
817
|
+
const LIGHT_ACCENT_LUMINANCE = .45;
|
|
818
|
+
/**
|
|
819
|
+
* Text colour for a button filled with `hex`: white on dark brands, near-black
|
|
820
|
+
* on light ones, so a yellow or pastel brand colour does not leave the
|
|
821
|
+
* launcher's icon and the Send label white-on-white. Null for anything that is
|
|
822
|
+
* not a plain hex colour, which keeps the stylesheet's own contrast colour.
|
|
823
|
+
*/
|
|
824
|
+
function accentContrastFor(hex) {
|
|
825
|
+
if (!HEX_COLOR.test(hex)) return null;
|
|
826
|
+
const digits = hex.length === 4 ? [...hex.slice(1)].map((digit) => digit + digit).join("") : hex.slice(1);
|
|
827
|
+
const [red = 0, green = 0, blue = 0] = [
|
|
828
|
+
0,
|
|
829
|
+
2,
|
|
830
|
+
4
|
|
831
|
+
].map((offset) => {
|
|
832
|
+
const value = Number.parseInt(digits.slice(offset, offset + 2), 16) / 255;
|
|
833
|
+
return value <= .03928 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4;
|
|
834
|
+
});
|
|
835
|
+
return .2126 * red + .7152 * green + .0722 * blue > LIGHT_ACCENT_LUMINANCE ? "#111827" : "#ffffff";
|
|
836
|
+
}
|
|
837
|
+
/** The session's brand colour, or the remembered one until a session lands. */
|
|
838
|
+
function brandColorOf(session, cached) {
|
|
839
|
+
if (session) return session.widget.config.brandColor ?? null;
|
|
840
|
+
return cached?.config.brandColor ?? null;
|
|
841
|
+
}
|
|
842
|
+
function useThemeStyle(brandColor, style) {
|
|
580
843
|
return useMemo(() => {
|
|
581
|
-
const
|
|
582
|
-
if (!brandColor) return style;
|
|
844
|
+
const hostSetsAccent = Boolean(style && "--clivly-chat-accent" in style);
|
|
845
|
+
if (!brandColor || hostSetsAccent) return style;
|
|
846
|
+
const contrast = accentContrastFor(brandColor);
|
|
583
847
|
return {
|
|
584
848
|
["--clivly-chat-accent"]: brandColor,
|
|
849
|
+
...contrast ? { ["--clivly-chat-accent-contrast"]: contrast } : {},
|
|
585
850
|
...style
|
|
586
851
|
};
|
|
587
|
-
}, [
|
|
852
|
+
}, [brandColor, style]);
|
|
588
853
|
}
|
|
589
854
|
/**
|
|
590
855
|
* Track the theme the widget should paint.
|
|
@@ -658,12 +923,21 @@ function automationBadge(message) {
|
|
|
658
923
|
* root's `data-clivly-launcher` (see WIDGET_STYLES); the label is always its
|
|
659
924
|
* accessible name, and `aria-controls` names the panel only while it exists.
|
|
660
925
|
*/
|
|
661
|
-
function Launcher({ label, onToggle, open, panelId }) {
|
|
926
|
+
function Launcher({ label, onIntent, onToggle, open, panelId }) {
|
|
662
927
|
return /* @__PURE__ */ jsxs("button", {
|
|
663
928
|
"aria-controls": open ? panelId : void 0,
|
|
664
929
|
"aria-expanded": open,
|
|
665
930
|
className: "clivly-chat-widget__launcher",
|
|
666
931
|
onClick: onToggle,
|
|
932
|
+
onFocus: () => {
|
|
933
|
+
onIntent("likely");
|
|
934
|
+
},
|
|
935
|
+
onPointerDown: () => {
|
|
936
|
+
onIntent("certain");
|
|
937
|
+
},
|
|
938
|
+
onPointerEnter: (event) => {
|
|
939
|
+
if (event.pointerType === "mouse") onIntent("likely");
|
|
940
|
+
},
|
|
667
941
|
type: "button",
|
|
668
942
|
children: [/* @__PURE__ */ jsx(LauncherIcon, { open }), /* @__PURE__ */ jsx("span", {
|
|
669
943
|
className: "clivly-chat-widget__launcher-label",
|
|
@@ -808,9 +1082,14 @@ function composerStatusText(hasTransport, isReady, reconnecting) {
|
|
|
808
1082
|
return reconnecting ? "Reconnecting…" : "Connecting…";
|
|
809
1083
|
}
|
|
810
1084
|
/**
|
|
811
|
-
* Guided options
|
|
812
|
-
*
|
|
813
|
-
*
|
|
1085
|
+
* Guided options, docked above the composer rather than inside the transcript.
|
|
1086
|
+
* In the transcript they sat below the greeting and the scroll-to-newest
|
|
1087
|
+
* pinned them there, so on a phone six wrapped chips pushed the greeting and
|
|
1088
|
+
* the answer above the fold. Docked, they are one compact strip (scrolling
|
|
1089
|
+
* sideways on narrow screens) and the conversation keeps the space.
|
|
1090
|
+
*
|
|
1091
|
+
* Native buttons in a labelled group, so they are reachable with Tab and
|
|
1092
|
+
* operable with Enter/Space; free text stays available beside them.
|
|
814
1093
|
*/
|
|
815
1094
|
function SupportTopics({ disabled, onSelect, topics }) {
|
|
816
1095
|
return /* @__PURE__ */ jsx("ul", {
|
|
@@ -827,9 +1106,64 @@ function SupportTopics({ disabled, onSelect, topics }) {
|
|
|
827
1106
|
}) }, topic.id))
|
|
828
1107
|
});
|
|
829
1108
|
}
|
|
830
|
-
/**
|
|
831
|
-
|
|
832
|
-
|
|
1109
|
+
/**
|
|
1110
|
+
* Topics are offered only while automation is answering this conversation.
|
|
1111
|
+
* Before the session lands, the remembered presentation stands in so the
|
|
1112
|
+
* strip is there on the first frame; it shows only if this browser last saw
|
|
1113
|
+
* them offered, so a visitor already talking to a person does not see them
|
|
1114
|
+
* flash up and vanish.
|
|
1115
|
+
*/
|
|
1116
|
+
function visibleTopics(session, state, cached) {
|
|
1117
|
+
if (session) return state === "automated" ? session.support?.topics ?? [] : [];
|
|
1118
|
+
return cached?.supportState === "automated" ? cached.topics : [];
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* What the open panel shows, derived from the session or — before it lands —
|
|
1122
|
+
* from the host's identity and the remembered presentation.
|
|
1123
|
+
*
|
|
1124
|
+
* A host-identified visitor never sees the form: while their session loads the
|
|
1125
|
+
* panel already shows the conversation shell — title, topics and a composer
|
|
1126
|
+
* they can type into — so opening the widget is not a blank wait. A failed
|
|
1127
|
+
* bootstrap sets `error` and falls back to the form, the recoverable state.
|
|
1128
|
+
*/
|
|
1129
|
+
function panelView({ cachedPresentation, defaultVisitor, error, session, supportState }) {
|
|
1130
|
+
const awaitingHostSession = !session && isCompleteVisitor(defaultVisitor) && !error;
|
|
1131
|
+
const showComposer = Boolean(session) || awaitingHostSession;
|
|
1132
|
+
const greeting = session?.widget.config.greeting ?? cachedPresentation?.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
1133
|
+
return {
|
|
1134
|
+
awaitingHostSession,
|
|
1135
|
+
greeting,
|
|
1136
|
+
panelTitle: session?.widget.name ?? cachedPresentation?.name ?? "Start a conversation",
|
|
1137
|
+
sessionSummary: awaitingHostSession && defaultVisitor ? `${defaultVisitor.name?.trim()} • ${defaultVisitor.email?.trim()}` : sessionSummaryText(session, greeting),
|
|
1138
|
+
showComposer,
|
|
1139
|
+
topics: showComposer ? visibleTopics(session, supportState, cachedPresentation) : []
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Who to start a session for ahead of the click, or null to wait for it. Only
|
|
1144
|
+
* a host-identified visitor; and on a mere hover or focus only one with a
|
|
1145
|
+
* stored thread to resume.
|
|
1146
|
+
*/
|
|
1147
|
+
function prefetchIdentity(intent, { defaultVisitor, settled }, storedToken) {
|
|
1148
|
+
if (settled || !isCompleteVisitor(defaultVisitor)) return null;
|
|
1149
|
+
const identity = {
|
|
1150
|
+
email: defaultVisitor.email.trim(),
|
|
1151
|
+
name: defaultVisitor.name.trim()
|
|
1152
|
+
};
|
|
1153
|
+
if (intent === "likely" && !storedToken(identity.email)) return null;
|
|
1154
|
+
return identity;
|
|
1155
|
+
}
|
|
1156
|
+
function startSessionQuietly(startSession, identity) {
|
|
1157
|
+
if (!identity) return;
|
|
1158
|
+
startSession(identity, "host").catch(() => {});
|
|
1159
|
+
}
|
|
1160
|
+
/** The composer grows with its text up to this height, then scrolls. */
|
|
1161
|
+
const COMPOSER_MAX_HEIGHT_PX = 128;
|
|
1162
|
+
/** Size a textarea to its content, between its CSS min-height and the cap. */
|
|
1163
|
+
function fitTextarea(textarea) {
|
|
1164
|
+
if (!textarea) return;
|
|
1165
|
+
textarea.style.height = "auto";
|
|
1166
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, COMPOSER_MAX_HEIGHT_PX)}px`;
|
|
833
1167
|
}
|
|
834
1168
|
function composerPlaceholder(hasTopics) {
|
|
835
1169
|
return hasTopics ? "Or type your question" : "Write your message";
|
|
@@ -960,6 +1294,33 @@ function useLiveConnection({ actions, createTransport, refreshSession, session,
|
|
|
960
1294
|
}, [createTransport, session]);
|
|
961
1295
|
return reconnecting;
|
|
962
1296
|
}
|
|
1297
|
+
/** Who the host says is signed in: their normalised email, or null. */
|
|
1298
|
+
function hostIdentityOf(defaultVisitor) {
|
|
1299
|
+
return isCompleteVisitor(defaultVisitor) ? normalizeVisitorEmail(defaultVisitor.email) : null;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* The host's signed-in visitor changed under an open session — a sign-out, or
|
|
1303
|
+
* a different account signing in without a page reload. The session on
|
|
1304
|
+
* screen is the previous person's: drop it, its transcript and its socket, so
|
|
1305
|
+
* the next open starts as whoever is signed in now (chat widget session
|
|
1306
|
+
* review 2026-10-28, S1). A session the visitor started from the pre-chat
|
|
1307
|
+
* form is theirs and is left alone.
|
|
1308
|
+
*/
|
|
1309
|
+
function useResetOnHostIdentityChange({ generationRef, hostIdentity, identityRef, reset, sessionSourceRef }) {
|
|
1310
|
+
const resetRef = useRef(reset);
|
|
1311
|
+
useEffect(() => {
|
|
1312
|
+
resetRef.current = reset;
|
|
1313
|
+
});
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
const current = identityRef.current;
|
|
1316
|
+
if (!(current && sessionSourceRef.current === "host")) return;
|
|
1317
|
+
if (hostIdentity === normalizeVisitorEmail(current.email)) return;
|
|
1318
|
+
generationRef.current += 1;
|
|
1319
|
+
identityRef.current = null;
|
|
1320
|
+
sessionSourceRef.current = null;
|
|
1321
|
+
resetRef.current();
|
|
1322
|
+
}, [hostIdentity]);
|
|
1323
|
+
}
|
|
963
1324
|
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcher = "auto", launcherLabel = "Chat with us", placement = "bottom-right", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
964
1325
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
965
1326
|
const panelId = useId();
|
|
@@ -971,19 +1332,32 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
971
1332
|
const [session, setSession] = useState(null);
|
|
972
1333
|
const [supportState, setSupportState] = useState(null);
|
|
973
1334
|
const [assistantThinking, setAssistantThinking] = useState(false);
|
|
1335
|
+
const [cachedPresentation, setCachedPresentation] = useState(null);
|
|
1336
|
+
useEffect(() => {
|
|
1337
|
+
setCachedPresentation(readPresentation(storage, widgetId));
|
|
1338
|
+
}, [storage, widgetId]);
|
|
974
1339
|
const [visitor, setVisitor] = useState({
|
|
975
1340
|
email: defaultVisitor?.email ?? "",
|
|
976
1341
|
name: defaultVisitor?.name ?? ""
|
|
977
1342
|
});
|
|
978
1343
|
const transportRef = useRef(null);
|
|
979
1344
|
const listRef = useRef(null);
|
|
1345
|
+
const composerRef = useRef(null);
|
|
1346
|
+
useEffect(() => {
|
|
1347
|
+
fitTextarea(composerRef.current);
|
|
1348
|
+
}, [messageDraft]);
|
|
980
1349
|
const identityRef = useRef(null);
|
|
1350
|
+
const sessionSourceRef = useRef(null);
|
|
1351
|
+
const generationRef = useRef(0);
|
|
1352
|
+
const hostIdentity = hostIdentityOf(defaultVisitor);
|
|
1353
|
+
const latestHostIdentityRef = useRef(hostIdentity);
|
|
1354
|
+
latestHostIdentityRef.current = hostIdentity;
|
|
981
1355
|
useEffect(() => {
|
|
982
1356
|
if (createTransport) return;
|
|
983
1357
|
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");
|
|
984
1358
|
}, []);
|
|
985
1359
|
const showWaitingNotice = useWaitingNotice(messages);
|
|
986
|
-
const themeStyle = useThemeStyle(session, style);
|
|
1360
|
+
const themeStyle = useThemeStyle(brandColorOf(session, cachedPresentation), style);
|
|
987
1361
|
const resolvedTheme = useResolvedTheme(theme);
|
|
988
1362
|
useEffect(() => () => {
|
|
989
1363
|
transportRef.current?.close();
|
|
@@ -1004,12 +1378,15 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1004
1378
|
refreshSession: async (current) => {
|
|
1005
1379
|
const identity = identityRef.current;
|
|
1006
1380
|
if (!identity) throw new Error("No visitor to reconnect as.");
|
|
1381
|
+
const generation = generationRef.current;
|
|
1007
1382
|
const next = await getSession({
|
|
1008
1383
|
visitor: identity,
|
|
1009
1384
|
visitorToken: current.visitorToken,
|
|
1010
1385
|
widgetId
|
|
1011
1386
|
});
|
|
1012
|
-
|
|
1387
|
+
if (generation !== generationRef.current) return;
|
|
1388
|
+
writeVisitorToken(storage, widgetId, identity.email, next.visitorToken);
|
|
1389
|
+
writePresentation(storage, widgetId, presentationOf(next));
|
|
1013
1390
|
setSupportState(next.support?.state ?? null);
|
|
1014
1391
|
setSession(next);
|
|
1015
1392
|
},
|
|
@@ -1034,31 +1411,54 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1034
1411
|
isReady,
|
|
1035
1412
|
messages
|
|
1036
1413
|
]);
|
|
1037
|
-
const startSession = useCallback(async (identity) => {
|
|
1414
|
+
const startSession = useCallback(async (identity, source = "form") => {
|
|
1038
1415
|
setError(null);
|
|
1039
1416
|
setIsConnecting(true);
|
|
1417
|
+
const generation = generationRef.current;
|
|
1418
|
+
const isStale = () => generation !== generationRef.current || source === "host" && normalizeVisitorEmail(identity.email) !== latestHostIdentityRef.current;
|
|
1040
1419
|
try {
|
|
1041
1420
|
const nextSession = await getSession({
|
|
1042
1421
|
visitor: identity,
|
|
1043
|
-
visitorToken: storage
|
|
1422
|
+
visitorToken: readVisitorToken(storage, widgetId, identity.email),
|
|
1044
1423
|
widgetId
|
|
1045
1424
|
});
|
|
1046
|
-
|
|
1425
|
+
if (isStale()) return;
|
|
1426
|
+
writeVisitorToken(storage, widgetId, identity.email, nextSession.visitorToken);
|
|
1427
|
+
writePresentation(storage, widgetId, presentationOf(nextSession));
|
|
1047
1428
|
identityRef.current = identity;
|
|
1429
|
+
sessionSourceRef.current = source;
|
|
1048
1430
|
setSupportState(nextSession.support?.state ?? null);
|
|
1049
1431
|
setSession(nextSession);
|
|
1050
1432
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
1051
1433
|
} catch (sessionError) {
|
|
1434
|
+
if (isStale()) return;
|
|
1052
1435
|
const resolved = sessionError instanceof Error ? sessionError : /* @__PURE__ */ new Error("Failed to start the conversation.");
|
|
1053
1436
|
setError(resolved.message);
|
|
1054
1437
|
} finally {
|
|
1055
|
-
setIsConnecting(false);
|
|
1438
|
+
if (generation === generationRef.current) setIsConnecting(false);
|
|
1056
1439
|
}
|
|
1057
1440
|
}, [
|
|
1058
1441
|
getSession,
|
|
1059
1442
|
storage,
|
|
1060
1443
|
widgetId
|
|
1061
1444
|
]);
|
|
1445
|
+
useResetOnHostIdentityChange({
|
|
1446
|
+
generationRef,
|
|
1447
|
+
hostIdentity,
|
|
1448
|
+
identityRef,
|
|
1449
|
+
reset: () => {
|
|
1450
|
+
setSession(null);
|
|
1451
|
+
setMessages([]);
|
|
1452
|
+
setSupportState(null);
|
|
1453
|
+
setError(null);
|
|
1454
|
+
setIsConnecting(false);
|
|
1455
|
+
setVisitor({
|
|
1456
|
+
email: defaultVisitor?.email ?? "",
|
|
1457
|
+
name: defaultVisitor?.name ?? ""
|
|
1458
|
+
});
|
|
1459
|
+
},
|
|
1460
|
+
sessionSourceRef
|
|
1461
|
+
});
|
|
1062
1462
|
const submitVisitor = async (event) => {
|
|
1063
1463
|
event.preventDefault();
|
|
1064
1464
|
await startSession(visitor);
|
|
@@ -1069,7 +1469,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1069
1469
|
startSession({
|
|
1070
1470
|
email: defaultVisitor.email.trim(),
|
|
1071
1471
|
name: defaultVisitor.name.trim()
|
|
1072
|
-
}).catch(() => {});
|
|
1472
|
+
}, "host").catch(() => {});
|
|
1073
1473
|
}, [
|
|
1074
1474
|
defaultVisitor,
|
|
1075
1475
|
error,
|
|
@@ -1078,6 +1478,13 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1078
1478
|
session,
|
|
1079
1479
|
startSession
|
|
1080
1480
|
]);
|
|
1481
|
+
const prefetchSession = (intent) => {
|
|
1482
|
+
const identity = prefetchIdentity(intent, {
|
|
1483
|
+
defaultVisitor,
|
|
1484
|
+
settled: isOpen || Boolean(session) || isConnecting || Boolean(error)
|
|
1485
|
+
}, (email) => readVisitorToken(storage, widgetId, email));
|
|
1486
|
+
startSessionQuietly(startSession, identity);
|
|
1487
|
+
};
|
|
1081
1488
|
/**
|
|
1082
1489
|
* Show the visitor's message at once and hand it to the transport. A topic
|
|
1083
1490
|
* click goes as `topic.select` when the transport supports it, so the
|
|
@@ -1116,13 +1523,17 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1116
1523
|
sendVisitorMessage(body);
|
|
1117
1524
|
};
|
|
1118
1525
|
const canSend = Boolean(createTransport) && isReady;
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1526
|
+
const { awaitingHostSession, greeting, panelTitle, sessionSummary, showComposer, topics } = panelView({
|
|
1527
|
+
cachedPresentation,
|
|
1528
|
+
defaultVisitor,
|
|
1529
|
+
error,
|
|
1530
|
+
session,
|
|
1531
|
+
supportState
|
|
1532
|
+
});
|
|
1123
1533
|
return /* @__PURE__ */ jsxs("div", {
|
|
1124
1534
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
1125
1535
|
"data-clivly-launcher": launcher,
|
|
1536
|
+
"data-clivly-open": isOpen || void 0,
|
|
1126
1537
|
"data-clivly-placement": placement,
|
|
1127
1538
|
"data-clivly-theme": resolvedTheme,
|
|
1128
1539
|
style: themeStyle,
|
|
@@ -1130,6 +1541,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1130
1541
|
/* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
|
|
1131
1542
|
/* @__PURE__ */ jsx(Launcher, {
|
|
1132
1543
|
label: launcherLabel,
|
|
1544
|
+
onIntent: prefetchSession,
|
|
1133
1545
|
onToggle: () => {
|
|
1134
1546
|
setIsOpen((open) => !open);
|
|
1135
1547
|
},
|
|
@@ -1143,23 +1555,36 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1143
1555
|
children: [
|
|
1144
1556
|
/* @__PURE__ */ jsxs("header", {
|
|
1145
1557
|
className: "clivly-chat-widget__header",
|
|
1146
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1147
|
-
className: "clivly-chat-
|
|
1148
|
-
children:
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1558
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1559
|
+
className: "clivly-chat-widget__heading",
|
|
1560
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1561
|
+
className: "clivly-chat-widget__title",
|
|
1562
|
+
children: panelTitle
|
|
1563
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1564
|
+
className: "clivly-chat-widget__subtitle",
|
|
1565
|
+
children: sessionSummary
|
|
1566
|
+
})]
|
|
1567
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1153
1568
|
"aria-label": "Close chat",
|
|
1154
1569
|
className: "clivly-chat-widget__close",
|
|
1155
1570
|
onClick: () => {
|
|
1156
1571
|
setIsOpen(false);
|
|
1157
1572
|
},
|
|
1158
1573
|
type: "button",
|
|
1159
|
-
children: "
|
|
1574
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1575
|
+
"aria-hidden": "true",
|
|
1576
|
+
fill: "none",
|
|
1577
|
+
focusable: "false",
|
|
1578
|
+
stroke: "currentColor",
|
|
1579
|
+
strokeLinecap: "round",
|
|
1580
|
+
strokeWidth: 2,
|
|
1581
|
+
viewBox: "0 0 24 24",
|
|
1582
|
+
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
1583
|
+
})
|
|
1160
1584
|
})]
|
|
1161
1585
|
}),
|
|
1162
1586
|
/* @__PURE__ */ jsxs("div", {
|
|
1587
|
+
"aria-busy": awaitingHostSession,
|
|
1163
1588
|
"aria-label": "Conversation",
|
|
1164
1589
|
className: "clivly-chat-widget__body",
|
|
1165
1590
|
ref: listRef,
|
|
@@ -1174,13 +1599,6 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1174
1599
|
hasSession: Boolean(session),
|
|
1175
1600
|
messages
|
|
1176
1601
|
}),
|
|
1177
|
-
topics.length > 0 ? /* @__PURE__ */ jsx(SupportTopics, {
|
|
1178
|
-
disabled: !canSend,
|
|
1179
|
-
onSelect: (topic) => {
|
|
1180
|
-
sendVisitorMessage(topic.label, topic);
|
|
1181
|
-
},
|
|
1182
|
-
topics
|
|
1183
|
-
}) : null,
|
|
1184
1602
|
session ? /* @__PURE__ */ jsx(StatusNotice, {
|
|
1185
1603
|
assistantThinking,
|
|
1186
1604
|
email: session.contact?.email,
|
|
@@ -1188,39 +1606,63 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
1188
1606
|
}) : null
|
|
1189
1607
|
]
|
|
1190
1608
|
}),
|
|
1191
|
-
|
|
1609
|
+
topics.length > 0 ? /* @__PURE__ */ jsx(SupportTopics, {
|
|
1610
|
+
disabled: !canSend,
|
|
1611
|
+
onSelect: (topic) => {
|
|
1612
|
+
sendVisitorMessage(topic.label, topic);
|
|
1613
|
+
},
|
|
1614
|
+
topics
|
|
1615
|
+
}) : null,
|
|
1616
|
+
showComposer ? /* @__PURE__ */ jsxs("form", {
|
|
1192
1617
|
className: "clivly-chat-widget__composer",
|
|
1193
1618
|
onSubmit: submitMessage,
|
|
1194
|
-
children: [/* @__PURE__ */ jsxs("
|
|
1195
|
-
className: "clivly-chat-
|
|
1196
|
-
children: [/* @__PURE__ */
|
|
1197
|
-
className: "clivly-chat-
|
|
1198
|
-
children: "
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
event
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1619
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1620
|
+
className: "clivly-chat-widget__composer-row",
|
|
1621
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
1622
|
+
className: "clivly-chat-widget__field clivly-chat-widget__composer-field",
|
|
1623
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1624
|
+
className: "clivly-chat-widget__label clivly-chat-widget__visually-hidden",
|
|
1625
|
+
children: "Message"
|
|
1626
|
+
}), /* @__PURE__ */ jsx("textarea", {
|
|
1627
|
+
className: "clivly-chat-widget__textarea",
|
|
1628
|
+
onChange: (event) => {
|
|
1629
|
+
setMessageDraft(event.target.value);
|
|
1630
|
+
},
|
|
1631
|
+
onKeyDown: (event) => {
|
|
1632
|
+
if (!isSendKey(event)) return;
|
|
1633
|
+
event.preventDefault();
|
|
1634
|
+
if (messageDraft.trim() && canSend) submitMessage();
|
|
1635
|
+
},
|
|
1636
|
+
placeholder: composerPlaceholder(topics.length > 0),
|
|
1637
|
+
ref: composerRef,
|
|
1638
|
+
rows: 1,
|
|
1639
|
+
value: messageDraft
|
|
1640
|
+
})]
|
|
1641
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1642
|
+
"aria-label": "Send",
|
|
1643
|
+
className: "clivly-chat-widget__button clivly-chat-widget__send",
|
|
1644
|
+
disabled: !(messageDraft.trim() && canSend),
|
|
1645
|
+
type: "submit",
|
|
1646
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1647
|
+
"aria-hidden": "true",
|
|
1648
|
+
fill: "none",
|
|
1649
|
+
focusable: "false",
|
|
1650
|
+
stroke: "currentColor",
|
|
1651
|
+
strokeLinecap: "round",
|
|
1652
|
+
strokeLinejoin: "round",
|
|
1653
|
+
strokeWidth: 2,
|
|
1654
|
+
viewBox: "0 0 24 24",
|
|
1655
|
+
children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
|
|
1656
|
+
})
|
|
1211
1657
|
})]
|
|
1212
|
-
}), /* @__PURE__ */
|
|
1658
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1213
1659
|
className: "clivly-chat-widget__actions",
|
|
1214
|
-
|
|
1660
|
+
"data-ready": canSend || void 0,
|
|
1661
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1215
1662
|
"aria-live": "polite",
|
|
1216
1663
|
className: "clivly-chat-widget__subtitle",
|
|
1217
1664
|
children: composerStatusText(Boolean(createTransport), isReady, reconnecting)
|
|
1218
|
-
})
|
|
1219
|
-
className: "clivly-chat-widget__button",
|
|
1220
|
-
disabled: !(messageDraft.trim() && canSend),
|
|
1221
|
-
type: "submit",
|
|
1222
|
-
children: "Send"
|
|
1223
|
-
})]
|
|
1665
|
+
})
|
|
1224
1666
|
})]
|
|
1225
1667
|
}) : /* @__PURE__ */ jsxs("form", {
|
|
1226
1668
|
className: "clivly-chat-widget__form",
|
|
@@ -1313,8 +1755,7 @@ function PreChatForm() {
|
|
|
1313
1755
|
function ConversationView() {
|
|
1314
1756
|
return null;
|
|
1315
1757
|
}
|
|
1316
|
-
|
|
1317
|
-
//#region src/transport.ts
|
|
1758
|
+
// src/transport.ts
|
|
1318
1759
|
function isServerEvent(value) {
|
|
1319
1760
|
if (typeof value !== "object" || value === null) return false;
|
|
1320
1761
|
return typeof value.type === "string";
|
|
@@ -1416,8 +1857,7 @@ function createLoopbackTransport({ operatorName = "Clivly", operatorReply = (bod
|
|
|
1416
1857
|
};
|
|
1417
1858
|
};
|
|
1418
1859
|
}
|
|
1419
|
-
|
|
1420
|
-
//#region src/clivly-transport.ts
|
|
1860
|
+
// src/clivly-transport.ts
|
|
1421
1861
|
/**
|
|
1422
1862
|
* The transport a host app should use against a real Clivly backend.
|
|
1423
1863
|
*
|
|
@@ -1434,8 +1874,7 @@ function createClivlyTransport$1(options = {}) {
|
|
|
1434
1874
|
}
|
|
1435
1875
|
});
|
|
1436
1876
|
}
|
|
1437
|
-
|
|
1438
|
-
//#region src/layout.ts
|
|
1877
|
+
// src/layout.ts
|
|
1439
1878
|
const PLACEMENTS = [
|
|
1440
1879
|
"bottom-left",
|
|
1441
1880
|
"bottom-right",
|
|
@@ -1454,8 +1893,7 @@ function parsePlacement(value) {
|
|
|
1454
1893
|
function parseLauncher(value) {
|
|
1455
1894
|
return LAUNCHERS.find((launcher) => launcher === value) ?? "auto";
|
|
1456
1895
|
}
|
|
1457
|
-
|
|
1458
|
-
//#region src/session.ts
|
|
1896
|
+
// src/session.ts
|
|
1459
1897
|
function isWidgetSessionPayload(value) {
|
|
1460
1898
|
return typeof value === "object" && value !== null;
|
|
1461
1899
|
}
|
|
@@ -1477,8 +1915,7 @@ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
|
|
|
1477
1915
|
return data;
|
|
1478
1916
|
};
|
|
1479
1917
|
}
|
|
1480
|
-
|
|
1481
|
-
//#region src/browser-mount.tsx
|
|
1918
|
+
// src/browser-mount.tsx
|
|
1482
1919
|
const MOUNTED_DATA_ATTR = "clivlyMounted";
|
|
1483
1920
|
/**
|
|
1484
1921
|
* The identity a script-tag host already knows, read off the tag.
|
|
@@ -1570,11 +2007,9 @@ function autoMountChatWidgetScript$1(script = document.currentScript) {
|
|
|
1570
2007
|
if (!script || scriptAlreadyMounted(script)) return null;
|
|
1571
2008
|
return mountChatWidgetScript$1(script);
|
|
1572
2009
|
}
|
|
1573
|
-
|
|
1574
|
-
//#region src/index.ts
|
|
2010
|
+
// src/index.ts
|
|
1575
2011
|
const autoMountChatWidgetScript = autoMountChatWidgetScript$1;
|
|
1576
2012
|
const createClivlyTransport = createClivlyTransport$1;
|
|
1577
2013
|
const mountChatWidget = mountChatWidget$1;
|
|
1578
2014
|
const mountChatWidgetScript = mountChatWidgetScript$1;
|
|
1579
|
-
//#endregion
|
|
1580
2015
|
export { ChatLauncher, ChatPanel, ChatWidget, ConversationView, MessageComposer, PreChatForm, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript, resolveTheme };
|