@xapp/chat-widget 1.96.3 → 1.96.4

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/dist/index.es.js CHANGED
@@ -78,7 +78,9 @@ color.invertColorHEX = invertColorHEX;
78
78
  color.invertColorRgb = invertColorRgb;
79
79
  color.invertColor = invertColor;
80
80
  color.isSupportedColor = isSupportedColor;
81
+ color.usableColorOr = usableColorOr;
81
82
  color.readableTextOn = readableTextOn;
83
+ color.darkenColor = darkenColor;
82
84
  /**
83
85
  * Colour maths for building a widget theme from a business's own branding.
84
86
  *
@@ -187,6 +189,19 @@ function isSupportedColor(color) {
187
189
  }
188
190
  return parseRgb(color) !== null;
189
191
  }
192
+ /**
193
+ * The colour to theme with: the one given, or the fallback when we cannot work
194
+ * out what text reads on it.
195
+ *
196
+ * Every builder needs this and each was doing it by hand, which is how the
197
+ * chat widget ended up guarded and the other two not. A background whose
198
+ * contrast cannot be computed is one we must not use — the foreground would be
199
+ * a guess paired with an unknown, and on a dark unparseable colour that is
200
+ * invisible text.
201
+ */
202
+ function usableColorOr(color, fallback) {
203
+ return isSupportedColor(color) ? color : fallback;
204
+ }
190
205
  /**
191
206
  * The text colour that will read on this background: black or white, never
192
207
  * anything else.
@@ -226,6 +241,77 @@ function hexToRgb(hex) {
226
241
  b: parseInt(value.slice(4, 6), 16)
227
242
  };
228
243
  }
244
+ /**
245
+ * A shade of the input colour, darkened by `amount` percent (negative
246
+ * lightens). Ported alongside the rest so the form widget's theme does not
247
+ * need its own copy of it.
248
+ */
249
+ function darkenColor(colorIn, amount) {
250
+ if (!colorIn) {
251
+ return colorIn;
252
+ }
253
+ let r, g, b, a;
254
+ let colorOut;
255
+ try {
256
+ // Parse the color format
257
+ if (colorIn.startsWith('#')) {
258
+ // Hex format
259
+ const hexMatch = colorIn.match(/\w\w/g);
260
+ if (hexMatch) {
261
+ [r, g, b] = hexMatch.map(x => parseInt(x, 16));
262
+ }
263
+ }
264
+ else if (colorIn.startsWith('rgb')) {
265
+ // RGB or RGBA format
266
+ const rgba = parseRgb(colorIn);
267
+ if (rgba) {
268
+ ({ r, g, b, a } = rgba);
269
+ }
270
+ }
271
+ if (r === undefined || g === undefined || b === undefined) {
272
+ console.error(`Unsupported color format: ${colorIn}`);
273
+ return colorIn;
274
+ }
275
+ if (a !== undefined) {
276
+ // Adjust luminance for RGBA
277
+ // Luminance is on the 0-255 scale the channels are, so the clamp
278
+ // has to be too. Bounding it to 0-1 collapsed it to 1 for every
279
+ // real colour, which made the ratio tiny and crushed the result to
280
+ // near-black instead of darkening it: rgba(20, 52, 204, 0.8) at 20%
281
+ // came back rgba(0, 1, 3, 1).
282
+ const luminance = 0.3 * r + 0.59 * g + 0.11 * b;
283
+ let newLuminance = luminance * (1 - amount / 100);
284
+ newLuminance = Math.max(0, Math.min(255, newLuminance));
285
+ const ratio = newLuminance / (luminance || 1); // no division by zero
286
+ r = Math.round(r * ratio);
287
+ g = Math.round(g * ratio);
288
+ b = Math.round(b * ratio);
289
+ // Adjust alpha
290
+ a = a + amount / 100;
291
+ a = Math.max(0, Math.min(1, a)); // within 0 and 1
292
+ colorOut = `rgba(${r}, ${g}, ${b}, ${a})`;
293
+ }
294
+ else {
295
+ // Adjust the color for RGB or Hex
296
+ if (amount > 0) {
297
+ r = Math.max(0, r - amount);
298
+ g = Math.max(0, g - amount);
299
+ b = Math.max(0, b - amount);
300
+ }
301
+ else {
302
+ amount = Math.abs(amount); // positive for lightening
303
+ r = Math.min(255, r + amount);
304
+ g = Math.min(255, g + amount);
305
+ b = Math.min(255, b + amount);
306
+ }
307
+ colorOut = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
308
+ }
309
+ }
310
+ catch (e) {
311
+ console.error(`Unsupported color format: ${colorIn} - Error: ${e}`);
312
+ }
313
+ return colorOut;
314
+ }
229
315
 
230
316
  Object.defineProperty(buildChatWidgetChannel$1, "__esModule", { value: true });
231
317
  buildChatWidgetChannel$1.buildChatWidgetChannel = buildChatWidgetChannel;
@@ -238,7 +324,7 @@ function buildChatWidgetChannel(props) {
238
324
  // Only a colour whose contrast can actually be computed, so the background
239
325
  // and the text on it are always a matched pair rather than one scraped
240
326
  // value and one guess.
241
- const primaryColor = (0, color_1.isSupportedColor)(props.primaryColor) ? props.primaryColor : DEFAULT_PRIMARY;
327
+ const primaryColor = (0, color_1.usableColorOr)(props.primaryColor, DEFAULT_PRIMARY);
242
328
  return {
243
329
  type: "chat-widget",
244
330
  // MCP by default. The widget runs on a public page, so homeowner mode
@@ -252,7 +338,7 @@ function buildChatWidgetChannel(props) {
252
338
  mode: "homeowner"
253
339
  }
254
340
  },
255
- theme: buildChatTheme(primaryColor),
341
+ theme: buildChatTheme(primaryColor, props.secondaryColor),
256
342
  actionBar: buildActionBar({ apiUrl, formWidgetKey, phoneNumber })
257
343
  };
258
344
  }
@@ -303,19 +389,49 @@ function buildActionBar(props) {
303
389
  };
304
390
  }
305
391
  /**
306
- * A theme derived from the business's own colour.
392
+ * The theme a new chat widget starts with, from the business's own colour.
307
393
  *
308
- * Only the text colour is computed rather than chosen: which of black or white
309
- * reads on a given background is arithmetic, and getting it wrong ships a
310
- * widget nobody can read.
394
+ * A faithful port of stentor-client's buildDefaultChatTheme, deliberately: the
395
+ * point of moving it is that onboarding can call this instead of its own copy,
396
+ * and that migration should not restyle every widget created afterwards. A
397
+ * thinner theme would have quietly dropped the CTA, message, input, footer and
398
+ * button styling that new widgets have today.
399
+ *
400
+ * The one change is how the contrast colour is derived — see below.
311
401
  */
312
- function buildChatTheme(primaryColor) {
313
- const onPrimary = (0, color_1.readableTextOn)(primaryColor);
402
+ function buildChatTheme(primaryColor, secondaryColor) {
403
+ const fontFamily = "RobotoRegular";
404
+ // secondaryColor is the colour of text sitting ON primaryColor — header
405
+ // title, subtitle, footer branding. Left unset it is computed, which is
406
+ // what makes the theme readable against whatever colour was scraped off
407
+ // the business's own site.
408
+ if (!secondaryColor) {
409
+ secondaryColor = (0, color_1.readableTextOn)(primaryColor);
410
+ }
314
411
  return {
315
412
  zIndex: "10000",
316
413
  border: {
317
414
  color: primaryColor,
318
- radius: "10px"
415
+ radius: "10px",
416
+ },
417
+ carousel: {
418
+ button: {
419
+ color: primaryColor,
420
+ },
421
+ subtitle: {
422
+ color: "#979797",
423
+ fontFamily,
424
+ fontSize: "12px",
425
+ fontStyle: "normal",
426
+ fontWeight: "normal",
427
+ },
428
+ title: {
429
+ color: "#000",
430
+ fontFamily,
431
+ fontSize: "14px",
432
+ fontStyle: "normal",
433
+ fontWeight: "bold",
434
+ },
319
435
  },
320
436
  chatButton: {
321
437
  background: primaryColor,
@@ -323,18 +439,147 @@ function buildChatTheme(primaryColor) {
323
439
  bottom: "10px",
324
440
  left: "10px",
325
441
  right: "10px",
326
- top: "10px"
327
- }
442
+ top: "10px",
443
+ },
328
444
  },
329
445
  content: {
330
- background: "#ffffff"
446
+ background: "#ffffff",
447
+ },
448
+ cta: {
449
+ background: "#e1e2e1",
450
+ text: {
451
+ // Use a dark color that contrasts well with the light grey background
452
+ // instead of primaryColor which may not have sufficient contrast
453
+ color: "#333333",
454
+ fontFamily,
455
+ fontSize: "14px",
456
+ },
457
+ },
458
+ footer: {
459
+ background: primaryColor,
460
+ border: {
461
+ color: primaryColor,
462
+ radius: "0px",
463
+ width: "0px",
464
+ },
465
+ branding: {
466
+ text: {
467
+ color: "#ffffff",
468
+ fontFamily,
469
+ fontSize: "14px",
470
+ fontWeight: "normal",
471
+ fontStyle: "normal",
472
+ },
473
+ },
331
474
  },
332
475
  header: {
333
476
  background: primaryColor,
477
+ avatarBackground: "transparent",
478
+ border: {
479
+ color: primaryColor,
480
+ radius: "0px",
481
+ width: "0px",
482
+ },
334
483
  text: {
335
- color: onPrimary
336
- }
337
- }
484
+ color: secondaryColor,
485
+ fontFamily,
486
+ fontSize: "16px",
487
+ fontWeight: "normal",
488
+ fontStyle: "normal",
489
+ },
490
+ subtitle: {
491
+ color: secondaryColor,
492
+ fontFamily,
493
+ fontSize: "10px",
494
+ fontWeight: "normal",
495
+ fontStyle: "normal",
496
+ },
497
+ },
498
+ input: {
499
+ background: "#ffffff",
500
+ border: {
501
+ radius: "20px",
502
+ },
503
+ text: {
504
+ color: "#000000",
505
+ fontFamily,
506
+ fontSize: "14px",
507
+ },
508
+ placeholder: {
509
+ color: "#e1e2e1",
510
+ fontFamily,
511
+ fontSize: "14px",
512
+ fontWeight: "normal",
513
+ fontStyle: "normal",
514
+ },
515
+ },
516
+ textTypingStatus: {
517
+ text: {
518
+ color: "#bfbfbf",
519
+ fontFamily,
520
+ fontSize: "14px",
521
+ fontWeight: "normal",
522
+ fontStyle: "normal",
523
+ },
524
+ },
525
+ menu: {
526
+ item: {
527
+ height: "24px",
528
+ text: {
529
+ fontSize: "14px",
530
+ },
531
+ },
532
+ },
533
+ menuButton: {
534
+ color: "#ffffff",
535
+ },
536
+ messages: {
537
+ maxWidth: "400px",
538
+ mine: {
539
+ bubbleColor: "#f2f2f2",
540
+ text: {
541
+ color: "#454545",
542
+ fontFamily,
543
+ fontSize: "18px",
544
+ fontStyle: "normal",
545
+ fontWeight: "normal",
546
+ },
547
+ },
548
+ others: {
549
+ bubbleColor: "#e1e2e1",
550
+ text: {
551
+ color: "#262726",
552
+ fontFamily,
553
+ fontSize: "18px",
554
+ fontStyle: "normal",
555
+ fontWeight: "normal",
556
+ },
557
+ },
558
+ padding: {
559
+ bottom: "0px",
560
+ left: "20px",
561
+ right: "16px",
562
+ top: "16px",
563
+ },
564
+ },
565
+ refreshButton: {
566
+ color: secondaryColor,
567
+ },
568
+ minimizeButton: {
569
+ color: secondaryColor,
570
+ },
571
+ cancelButton: {
572
+ color: secondaryColor,
573
+ },
574
+ primaryColor: primaryColor,
575
+ sendButton: {
576
+ color: "#505050",
577
+ width: "20px",
578
+ },
579
+ size: {
580
+ width: "500px",
581
+ height: "700px",
582
+ },
338
583
  };
339
584
  }
340
585