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