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