@customerhero/js 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,7 +7,29 @@ var DEFAULTS = {
7
7
  position: "bottom-right",
8
8
  placeholderText: "Type your message...",
9
9
  welcomeMessage: "Hi! How can I help you today?",
10
- title: "CustomerHero"
10
+ title: "CustomerHero",
11
+ // Appearance pack (B1–B6) defaults.
12
+ colorScheme: "light",
13
+ // Dark fallbacks used when the effective scheme is dark and no per-chatbot
14
+ // dark color is configured. Operators can — and should — override these.
15
+ primaryColorDark: "#A78BFA",
16
+ backgroundColorDark: "#0F172A",
17
+ textColorDark: "#E5E7EB",
18
+ size: "default",
19
+ cornerStyle: "rounded",
20
+ offsetBottom: 20,
21
+ offsetSide: 20,
22
+ zIndex: 99999
23
+ };
24
+ var SIZE_PRESETS = {
25
+ compact: { bubble: 48, width: 320, height: 480, fontSize: 13 },
26
+ default: { bubble: 56, width: 380, height: 520, fontSize: 14 },
27
+ large: { bubble: 64, width: 440, height: 600, fontSize: 15 }
28
+ };
29
+ var CORNER_RADIUS = {
30
+ soft: 8,
31
+ rounded: 16,
32
+ square: 0
11
33
  };
12
34
 
13
35
  // src/i18n/locales/en.ts
@@ -36,6 +58,8 @@ var en = {
36
58
  attach_menu_open: "Add attachment",
37
59
  attach_photo: "Choose file",
38
60
  drop_files_here: "Drop files here",
61
+ incident_dismiss: "Dismiss",
62
+ incident_default_link_label: "Learn more",
39
63
  attachment_unsupported_type: "Unsupported file type"
40
64
  };
41
65
 
@@ -65,6 +89,8 @@ var es = {
65
89
  attach_menu_open: "A\xF1adir adjunto",
66
90
  attach_photo: "Elegir archivo",
67
91
  drop_files_here: "Suelta los archivos aqu\xED",
92
+ incident_dismiss: "Descartar",
93
+ incident_default_link_label: "M\xE1s informaci\xF3n",
68
94
  attachment_unsupported_type: "Tipo de archivo no admitido"
69
95
  };
70
96
 
@@ -94,6 +120,8 @@ var ptBR = {
94
120
  attach_menu_open: "Adicionar anexo",
95
121
  attach_photo: "Escolher arquivo",
96
122
  drop_files_here: "Solte os arquivos aqui",
123
+ incident_dismiss: "Dispensar",
124
+ incident_default_link_label: "Saiba mais",
97
125
  attachment_unsupported_type: "Tipo de arquivo n\xE3o suportado"
98
126
  };
99
127
 
@@ -123,6 +151,8 @@ var ptPT = {
123
151
  attach_menu_open: "Adicionar anexo",
124
152
  attach_photo: "Escolher ficheiro",
125
153
  drop_files_here: "Largue os ficheiros aqui",
154
+ incident_dismiss: "Dispensar",
155
+ incident_default_link_label: "Saiba mais",
126
156
  attachment_unsupported_type: "Tipo de ficheiro n\xE3o suportado"
127
157
  };
128
158
 
@@ -152,6 +182,8 @@ var fr = {
152
182
  attach_menu_open: "Ajouter une pi\xE8ce jointe",
153
183
  attach_photo: "Choisir un fichier",
154
184
  drop_files_here: "D\xE9posez les fichiers ici",
185
+ incident_dismiss: "Ignorer",
186
+ incident_default_link_label: "En savoir plus",
155
187
  attachment_unsupported_type: "Type de fichier non pris en charge"
156
188
  };
157
189
 
@@ -181,6 +213,8 @@ var de = {
181
213
  attach_menu_open: "Anhang hinzuf\xFCgen",
182
214
  attach_photo: "Datei ausw\xE4hlen",
183
215
  drop_files_here: "Dateien hier ablegen",
216
+ incident_dismiss: "Schlie\xDFen",
217
+ incident_default_link_label: "Mehr erfahren",
184
218
  attachment_unsupported_type: "Dateityp nicht unterst\xFCtzt"
185
219
  };
186
220
 
@@ -210,6 +244,8 @@ var it = {
210
244
  attach_menu_open: "Aggiungi allegato",
211
245
  attach_photo: "Scegli file",
212
246
  drop_files_here: "Trascina qui i file",
247
+ incident_dismiss: "Chiudi",
248
+ incident_default_link_label: "Scopri di pi\xF9",
213
249
  attachment_unsupported_type: "Tipo di file non supportato"
214
250
  };
215
251
 
@@ -239,6 +275,8 @@ var nl = {
239
275
  attach_menu_open: "Bijlage toevoegen",
240
276
  attach_photo: "Bestand kiezen",
241
277
  drop_files_here: "Sleep bestanden hier",
278
+ incident_dismiss: "Sluiten",
279
+ incident_default_link_label: "Meer informatie",
242
280
  attachment_unsupported_type: "Bestandstype niet ondersteund"
243
281
  };
244
282
 
@@ -268,6 +306,8 @@ var pl = {
268
306
  attach_menu_open: "Dodaj za\u0142\u0105cznik",
269
307
  attach_photo: "Wybierz plik",
270
308
  drop_files_here: "Upu\u015B\u0107 pliki tutaj",
309
+ incident_dismiss: "Zamknij",
310
+ incident_default_link_label: "Dowiedz si\u0119 wi\u0119cej",
271
311
  attachment_unsupported_type: "Nieobs\u0142ugiwany typ pliku"
272
312
  };
273
313
 
@@ -297,6 +337,8 @@ var tr = {
297
337
  attach_menu_open: "Ek ekle",
298
338
  attach_photo: "Dosya se\xE7",
299
339
  drop_files_here: "Dosyalar\u0131 buraya b\u0131rak",
340
+ incident_dismiss: "Kapat",
341
+ incident_default_link_label: "Daha fazla bilgi",
300
342
  attachment_unsupported_type: "Desteklenmeyen dosya t\xFCr\xFC"
301
343
  };
302
344
 
@@ -326,6 +368,8 @@ var ar = {
326
368
  attach_menu_open: "\u0625\u0636\u0627\u0641\u0629 \u0645\u0631\u0641\u0642",
327
369
  attach_photo: "\u0627\u062E\u062A\u0631 \u0645\u0644\u0641\u064B\u0627",
328
370
  drop_files_here: "\u0623\u0641\u0644\u062A \u0627\u0644\u0645\u0644\u0641\u0627\u062A \u0647\u0646\u0627",
371
+ incident_dismiss: "\u0625\u063A\u0644\u0627\u0642",
372
+ incident_default_link_label: "\u0627\u0639\u0631\u0641 \u0627\u0644\u0645\u0632\u064A\u062F",
329
373
  attachment_unsupported_type: "\u0646\u0648\u0639 \u0627\u0644\u0645\u0644\u0641 \u063A\u064A\u0631 \u0645\u062F\u0639\u0648\u0645"
330
374
  };
331
375
 
@@ -355,6 +399,8 @@ var ja = {
355
399
  attach_menu_open: "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u3092\u8FFD\u52A0",
356
400
  attach_photo: "\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E",
357
401
  drop_files_here: "\u30D5\u30A1\u30A4\u30EB\u3092\u3053\u3053\u306B\u30C9\u30ED\u30C3\u30D7",
402
+ incident_dismiss: "\u9589\u3058\u308B",
403
+ incident_default_link_label: "\u8A73\u7D30\u3092\u898B\u308B",
358
404
  attachment_unsupported_type: "\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F"
359
405
  };
360
406
 
@@ -384,6 +430,8 @@ var ko = {
384
430
  attach_menu_open: "\uCCA8\uBD80 \uD30C\uC77C \uCD94\uAC00",
385
431
  attach_photo: "\uD30C\uC77C \uC120\uD0DD",
386
432
  drop_files_here: "\uD30C\uC77C\uC744 \uC5EC\uAE30\uC5D0 \uB193\uAE30",
433
+ incident_dismiss: "\uB2EB\uAE30",
434
+ incident_default_link_label: "\uC790\uC138\uD788 \uC54C\uC544\uBCF4\uAE30",
387
435
  attachment_unsupported_type: "\uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD"
388
436
  };
389
437
 
@@ -413,6 +461,8 @@ var zhCN = {
413
461
  attach_menu_open: "\u6DFB\u52A0\u9644\u4EF6",
414
462
  attach_photo: "\u9009\u62E9\u6587\u4EF6",
415
463
  drop_files_here: "\u5C06\u6587\u4EF6\u62D6\u653E\u5230\u6B64\u5904",
464
+ incident_dismiss: "\u5173\u95ED",
465
+ incident_default_link_label: "\u4E86\u89E3\u66F4\u591A",
416
466
  attachment_unsupported_type: "\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B"
417
467
  };
418
468
 
@@ -442,6 +492,8 @@ var zhTW = {
442
492
  attach_menu_open: "\u65B0\u589E\u9644\u4EF6",
443
493
  attach_photo: "\u9078\u64C7\u6A94\u6848",
444
494
  drop_files_here: "\u5C07\u6A94\u6848\u62D6\u653E\u5230\u6B64\u8655",
495
+ incident_dismiss: "\u95DC\u9589",
496
+ incident_default_link_label: "\u77AD\u89E3\u66F4\u591A",
445
497
  attachment_unsupported_type: "\u4E0D\u652F\u63F4\u7684\u6A94\u6848\u985E\u578B"
446
498
  };
447
499
 
@@ -945,7 +997,14 @@ function startTriggersRuntime(options) {
945
997
  }
946
998
 
947
999
  // src/client.ts
1000
+ function clampInt(value, min, max, fallback) {
1001
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
1002
+ return Math.max(min, Math.min(max, Math.trunc(value)));
1003
+ }
948
1004
  function resolveConfig(userConfig, fetched) {
1005
+ const launcherUser = userConfig.launcher ?? {};
1006
+ const launcherFetched = fetched?.launcher ?? {};
1007
+ const offsetUser = userConfig.offset ?? {};
949
1008
  return {
950
1009
  chatbotId: userConfig.chatbotId,
951
1010
  apiBase: userConfig.apiBase ?? DEFAULTS.apiBase,
@@ -958,12 +1017,78 @@ function resolveConfig(userConfig, fetched) {
958
1017
  title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
959
1018
  avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
960
1019
  suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? [],
961
- stringOverrides: fetched?.stringOverrides
1020
+ stringOverrides: fetched?.stringOverrides,
1021
+ // Appearance pack. Color palette + size + corner style + launcher all
1022
+ // come from the server widget_config (with host-side override). The
1023
+ // *runtime* knobs — colorScheme, offset, zIndex — are host-only because
1024
+ // they depend on the page the widget is embedded in, not the chatbot.
1025
+ primaryColorDark: userConfig.primaryColorDark ?? fetched?.primaryColorDark,
1026
+ backgroundColorDark: userConfig.backgroundColorDark ?? fetched?.backgroundColorDark,
1027
+ textColorDark: userConfig.textColorDark ?? fetched?.textColorDark,
1028
+ size: userConfig.size ?? fetched?.size ?? DEFAULTS.size,
1029
+ cornerStyle: userConfig.cornerStyle ?? fetched?.cornerStyle ?? DEFAULTS.cornerStyle,
1030
+ launcher: {
1031
+ iconUrl: launcherUser.iconUrl ?? launcherFetched.iconUrl,
1032
+ label: launcherUser.label ?? launcherFetched.label,
1033
+ showOnlineDot: launcherUser.showOnlineDot ?? launcherFetched.showOnlineDot ?? false
1034
+ },
1035
+ colorScheme: userConfig.colorScheme ?? DEFAULTS.colorScheme,
1036
+ offset: {
1037
+ bottom: clampInt(offsetUser.bottom, 0, 1e3, DEFAULTS.offsetBottom),
1038
+ side: clampInt(offsetUser.side, 0, 1e3, DEFAULTS.offsetSide)
1039
+ },
1040
+ zIndex: clampInt(userConfig.zIndex, 0, 2e9, DEFAULTS.zIndex),
1041
+ // Defaults to true — the widget shows the attach button unless the
1042
+ // chatbot explicitly opts out via widget_config or the host passes
1043
+ // allowAttachments=false. Server still enforces the same flag at the
1044
+ // upload endpoint either way.
1045
+ allowAttachments: userConfig.allowAttachments ?? fetched?.allowAttachments ?? true
962
1046
  };
963
1047
  }
1048
+ function sanitizeIncidentBanner(input) {
1049
+ if (!input || typeof input !== "object") return null;
1050
+ const raw = input;
1051
+ const sev = raw.severity;
1052
+ if (sev !== "info" && sev !== "warning" && sev !== "outage") return null;
1053
+ if (typeof raw.title !== "string" || raw.title.length === 0) return null;
1054
+ if (typeof raw.expiresAt === "string") {
1055
+ const t = Date.parse(raw.expiresAt);
1056
+ if (!Number.isNaN(t) && t <= Date.now()) return null;
1057
+ }
1058
+ const out = { severity: sev, title: raw.title };
1059
+ if (typeof raw.body === "string") out.body = raw.body;
1060
+ if (typeof raw.eta === "string") out.eta = raw.eta;
1061
+ if (raw.link && typeof raw.link === "object") {
1062
+ const link = raw.link;
1063
+ if (typeof link.url === "string") {
1064
+ out.link = { url: link.url };
1065
+ if (typeof link.label === "string") out.link.label = link.label;
1066
+ }
1067
+ }
1068
+ if (typeof raw.expiresAt === "string") out.expiresAt = raw.expiresAt;
1069
+ return out;
1070
+ }
1071
+ function bannerKey(banner) {
1072
+ if (!banner) return null;
1073
+ return JSON.stringify([
1074
+ banner.severity,
1075
+ banner.title,
1076
+ banner.body ?? "",
1077
+ banner.eta ?? "",
1078
+ banner.link?.url ?? "",
1079
+ banner.link?.label ?? "",
1080
+ banner.expiresAt ?? ""
1081
+ ]);
1082
+ }
964
1083
  function getStorage() {
965
1084
  try {
966
- return typeof window !== "undefined" ? window.localStorage : null;
1085
+ if (typeof window !== "undefined" && window.localStorage)
1086
+ return window.localStorage;
1087
+ if (typeof globalThis !== "undefined") {
1088
+ const ls = globalThis.localStorage;
1089
+ if (ls) return ls;
1090
+ }
1091
+ return null;
967
1092
  } catch {
968
1093
  return null;
969
1094
  }
@@ -1005,9 +1130,42 @@ var CustomerHeroChat = class {
1005
1130
  preChatSubmission: null,
1006
1131
  consent: this.readStoredConsent(),
1007
1132
  pendingTriggerId: null,
1008
- pendingPrefill: null
1133
+ pendingPrefill: null,
1134
+ incidentBanner: null,
1135
+ incidentBannerDismissed: false,
1136
+ readOnly: false
1009
1137
  };
1010
1138
  }
1139
+ /**
1140
+ * Mark the config as loaded and put the client into read-only preview
1141
+ * mode without hitting the API. Used by `@customerhero/react/preview` to
1142
+ * render the widget against a host-supplied config (the dashboard preview
1143
+ * pane). Public API consumers should not call this.
1144
+ *
1145
+ * Pass a config to re-resolve and update the rendered colors/size/launcher
1146
+ * in place. Callers should reuse the same client instance across config
1147
+ * changes so the open animation only fires once.
1148
+ *
1149
+ * @internal
1150
+ */
1151
+ __seedForPreview(config, extras) {
1152
+ const resolved = config ? resolveConfig(config) : this.state.config;
1153
+ if (config) this.userConfig = config;
1154
+ const seededMessages = resolved.welcomeMessage ? [{ role: "bot", content: resolved.welcomeMessage }] : [];
1155
+ const sanitizedBanner = extras && "banner" in extras ? sanitizeIncidentBanner(extras.banner ?? null) : this.state.incidentBanner;
1156
+ this.setState({
1157
+ config: resolved,
1158
+ configLoaded: true,
1159
+ configError: null,
1160
+ readOnly: true,
1161
+ isOpen: true,
1162
+ messages: seededMessages,
1163
+ incidentBanner: sanitizedBanner,
1164
+ // Reset the dismissed flag so toggling the banner on in the dashboard
1165
+ // re-renders it after a previous preview-side dismiss.
1166
+ incidentBannerDismissed: false
1167
+ });
1168
+ }
1011
1169
  // ── Proactive engagement state ─────────────────────────────────────
1012
1170
  triggersRuntime = null;
1013
1171
  preChatFormSubmitted = false;
@@ -1084,11 +1242,18 @@ var CustomerHeroChat = class {
1084
1242
  const resolved = resolveConfig(this.userConfig, fetched);
1085
1243
  const triggers = Array.isArray(fetched.triggers) ? fetched.triggers : [];
1086
1244
  const preChatForm = fetched.preChatForm ?? null;
1245
+ const incidentBanner = sanitizeIncidentBanner(fetched.incidentBanner);
1246
+ const prevBannerKey = bannerKey(this.state.incidentBanner);
1247
+ const nextBannerKey = bannerKey(incidentBanner);
1248
+ const dismissedFromStorage = nextBannerKey && nextBannerKey === this.readStoredBannerDismissal();
1249
+ const incidentBannerDismissed = nextBannerKey === prevBannerKey ? this.state.incidentBannerDismissed || !!dismissedFromStorage : !!dismissedFromStorage;
1087
1250
  this.setState({
1088
1251
  config: resolved,
1089
1252
  configLoaded: true,
1090
1253
  triggers,
1091
- preChatForm
1254
+ preChatForm,
1255
+ incidentBanner,
1256
+ incidentBannerDismissed
1092
1257
  });
1093
1258
  if (resolved.stringOverrides) this.rebuildTranslator();
1094
1259
  this.startTriggersRuntimeIfPossible();
@@ -1146,6 +1311,7 @@ var CustomerHeroChat = class {
1146
1311
  }
1147
1312
  }
1148
1313
  async sendMessage(message, options) {
1314
+ if (this.state.readOnly) return;
1149
1315
  const trimmed = message.trim();
1150
1316
  const attachmentTokens = options?.attachmentTokens ?? [];
1151
1317
  if (!trimmed || this.state.isLoading) return;
@@ -1513,6 +1679,34 @@ var CustomerHeroChat = class {
1513
1679
  setTraits(traits) {
1514
1680
  this.triggersRuntime?.setTraits(traits);
1515
1681
  }
1682
+ /** Hide the active incident banner for this visitor. Persisted in
1683
+ * localStorage so a refresh keeps it dismissed; resets automatically
1684
+ * when the operator changes the banner content. No-op when no banner
1685
+ * is showing. */
1686
+ dismissIncidentBanner() {
1687
+ const key = bannerKey(this.state.incidentBanner);
1688
+ if (!key) return;
1689
+ this.writeStoredBannerDismissal(key);
1690
+ this.setState({ incidentBannerDismissed: true });
1691
+ }
1692
+ readStoredBannerDismissal() {
1693
+ try {
1694
+ return this.storage?.getItem(
1695
+ `ch_incident_dismissed_${this.userConfig.chatbotId}`
1696
+ ) ?? null;
1697
+ } catch {
1698
+ return null;
1699
+ }
1700
+ }
1701
+ writeStoredBannerDismissal(key) {
1702
+ try {
1703
+ this.storage?.setItem(
1704
+ `ch_incident_dismissed_${this.userConfig.chatbotId}`,
1705
+ key
1706
+ );
1707
+ } catch {
1708
+ }
1709
+ }
1516
1710
  /** Submit pre-chat form answers. Synthesizes a customer record server-side
1517
1711
  * on the next sendMessage. Resumes any pending message that was deferred
1518
1712
  * while the form was open. */
@@ -1670,6 +1864,33 @@ function pickExtension(mime) {
1670
1864
  return "jpg";
1671
1865
  }
1672
1866
 
1867
+ // src/theme.ts
1868
+ function resolveScheme(colorScheme, prefersDark) {
1869
+ if (colorScheme === "dark") return "dark";
1870
+ if (colorScheme === "light") return "light";
1871
+ return prefersDark ? "dark" : "light";
1872
+ }
1873
+ function effectiveColors(config, scheme) {
1874
+ if (scheme === "dark") {
1875
+ return {
1876
+ primary: config.primaryColorDark ?? DEFAULTS.primaryColorDark,
1877
+ background: config.backgroundColorDark ?? DEFAULTS.backgroundColorDark,
1878
+ text: config.textColorDark ?? DEFAULTS.textColorDark
1879
+ };
1880
+ }
1881
+ return {
1882
+ primary: config.primaryColor,
1883
+ background: config.backgroundColor,
1884
+ text: config.textColor
1885
+ };
1886
+ }
1887
+ function sizePreset(size) {
1888
+ return SIZE_PRESETS[size];
1889
+ }
1890
+ function panelRadius(cornerStyle) {
1891
+ return CORNER_RADIUS[cornerStyle];
1892
+ }
1893
+
1673
1894
  // src/screenshot.ts
1674
1895
  var ScreenshotCancelled = class extends Error {
1675
1896
  constructor(message = "Screenshot cancelled") {
@@ -1803,8 +2024,10 @@ async function canvasToBlob(canvas, quality) {
1803
2024
  });
1804
2025
  }
1805
2026
  export {
2027
+ CORNER_RADIUS,
1806
2028
  CustomerHeroChat,
1807
2029
  DEFAULTS,
2030
+ SIZE_PRESETS,
1808
2031
  SUPPORTED_LOCALES,
1809
2032
  ScreenshotCancelled,
1810
2033
  ScreenshotUnavailable,
@@ -1812,9 +2035,13 @@ export {
1812
2035
  captureScreenshot,
1813
2036
  createTranslator,
1814
2037
  detectLocale,
2038
+ effectiveColors,
1815
2039
  evaluate,
1816
2040
  isRtlLocale,
2041
+ panelRadius,
1817
2042
  pickFire,
1818
2043
  resolveLocale,
2044
+ resolveScheme,
2045
+ sizePreset,
1819
2046
  startTriggersRuntime
1820
2047
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customerhero/js",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "description": "Framework-agnostic JavaScript client for the CustomerHero chat widget.",
6
6
  "keywords": [