@customerhero/js 2.2.0 → 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/README.md CHANGED
@@ -56,6 +56,25 @@ chat.identify({
56
56
  | `locale` | `string` | Widget locale (e.g. `"en"`, `"es"`). Auto-detected from browser if omitted. |
57
57
  | `suggestedMessages` | `string[]` | Quick-reply options shown before the first message. |
58
58
 
59
+ ### Appearance
60
+
61
+ | Option | Type | Description |
62
+ | ------------------------ | ----------------------------------- | --------------------------------------------------------------------------------- |
63
+ | `colorScheme` | `"auto" \| "light" \| "dark"` | `auto` follows the visitor's OS preference. Defaults to `light`. |
64
+ | `primaryColorDark` | `string` | Primary color used in dark mode. Only honoured when the effective scheme is dark. |
65
+ | `backgroundColorDark` | `string` | Background color used in dark mode. |
66
+ | `textColorDark` | `string` | Text color used in dark mode. |
67
+ | `size` | `"compact" \| "default" \| "large"` | Launcher diameter, panel dimensions, and base font size. |
68
+ | `cornerStyle` | `"soft" \| "rounded" \| "square"` | Panel border-radius preset. |
69
+ | `launcher.iconUrl` | `string` | Custom launcher icon URL (replaces the default chat-bubble glyph). |
70
+ | `launcher.label` | `string` | CTA label next to the launcher (turns the bubble into a pill). Max 60 chars. |
71
+ | `launcher.showOnlineDot` | `boolean` | Show a small green dot on the launcher when agents are available. |
72
+ | `offset.bottom` | `number` | Pixel offset from the bottom edge. 0–1000. Defaults to 20. |
73
+ | `offset.side` | `number` | Pixel offset from the side (mirrors `position`). 0–1000. Defaults to 20. |
74
+ | `zIndex` | `number` | Z-index override. Defaults to 99999. Capped at 2 000 000 000. |
75
+
76
+ Dark colors are never auto-derived from `primaryColor`/`backgroundColor`/`textColor` — set them explicitly when enabling dark or auto modes.
77
+
59
78
  ## License
60
79
 
61
80
  MIT
package/dist/index.cjs CHANGED
@@ -20,8 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CORNER_RADIUS: () => CORNER_RADIUS,
23
24
  CustomerHeroChat: () => CustomerHeroChat,
24
25
  DEFAULTS: () => DEFAULTS,
26
+ SIZE_PRESETS: () => SIZE_PRESETS,
25
27
  SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
26
28
  ScreenshotCancelled: () => ScreenshotCancelled,
27
29
  ScreenshotUnavailable: () => ScreenshotUnavailable,
@@ -29,10 +31,14 @@ __export(index_exports, {
29
31
  captureScreenshot: () => captureScreenshot,
30
32
  createTranslator: () => createTranslator,
31
33
  detectLocale: () => detectLocale,
34
+ effectiveColors: () => effectiveColors,
32
35
  evaluate: () => evaluate,
33
36
  isRtlLocale: () => isRtlLocale,
37
+ panelRadius: () => panelRadius,
34
38
  pickFire: () => pickFire,
35
39
  resolveLocale: () => resolveLocale,
40
+ resolveScheme: () => resolveScheme,
41
+ sizePreset: () => sizePreset,
36
42
  startTriggersRuntime: () => startTriggersRuntime
37
43
  });
38
44
  module.exports = __toCommonJS(index_exports);
@@ -46,7 +52,29 @@ var DEFAULTS = {
46
52
  position: "bottom-right",
47
53
  placeholderText: "Type your message...",
48
54
  welcomeMessage: "Hi! How can I help you today?",
49
- title: "CustomerHero"
55
+ title: "CustomerHero",
56
+ // Appearance pack (B1–B6) defaults.
57
+ colorScheme: "light",
58
+ // Dark fallbacks used when the effective scheme is dark and no per-chatbot
59
+ // dark color is configured. Operators can — and should — override these.
60
+ primaryColorDark: "#A78BFA",
61
+ backgroundColorDark: "#0F172A",
62
+ textColorDark: "#E5E7EB",
63
+ size: "default",
64
+ cornerStyle: "rounded",
65
+ offsetBottom: 20,
66
+ offsetSide: 20,
67
+ zIndex: 99999
68
+ };
69
+ var SIZE_PRESETS = {
70
+ compact: { bubble: 48, width: 320, height: 480, fontSize: 13 },
71
+ default: { bubble: 56, width: 380, height: 520, fontSize: 14 },
72
+ large: { bubble: 64, width: 440, height: 600, fontSize: 15 }
73
+ };
74
+ var CORNER_RADIUS = {
75
+ soft: 8,
76
+ rounded: 16,
77
+ square: 0
50
78
  };
51
79
 
52
80
  // src/i18n/locales/en.ts
@@ -1014,7 +1042,14 @@ function startTriggersRuntime(options) {
1014
1042
  }
1015
1043
 
1016
1044
  // src/client.ts
1045
+ function clampInt(value, min, max, fallback) {
1046
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
1047
+ return Math.max(min, Math.min(max, Math.trunc(value)));
1048
+ }
1017
1049
  function resolveConfig(userConfig, fetched) {
1050
+ const launcherUser = userConfig.launcher ?? {};
1051
+ const launcherFetched = fetched?.launcher ?? {};
1052
+ const offsetUser = userConfig.offset ?? {};
1018
1053
  return {
1019
1054
  chatbotId: userConfig.chatbotId,
1020
1055
  apiBase: userConfig.apiBase ?? DEFAULTS.apiBase,
@@ -1027,7 +1062,32 @@ function resolveConfig(userConfig, fetched) {
1027
1062
  title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
1028
1063
  avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
1029
1064
  suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? [],
1030
- stringOverrides: fetched?.stringOverrides
1065
+ stringOverrides: fetched?.stringOverrides,
1066
+ // Appearance pack. Color palette + size + corner style + launcher all
1067
+ // come from the server widget_config (with host-side override). The
1068
+ // *runtime* knobs — colorScheme, offset, zIndex — are host-only because
1069
+ // they depend on the page the widget is embedded in, not the chatbot.
1070
+ primaryColorDark: userConfig.primaryColorDark ?? fetched?.primaryColorDark,
1071
+ backgroundColorDark: userConfig.backgroundColorDark ?? fetched?.backgroundColorDark,
1072
+ textColorDark: userConfig.textColorDark ?? fetched?.textColorDark,
1073
+ size: userConfig.size ?? fetched?.size ?? DEFAULTS.size,
1074
+ cornerStyle: userConfig.cornerStyle ?? fetched?.cornerStyle ?? DEFAULTS.cornerStyle,
1075
+ launcher: {
1076
+ iconUrl: launcherUser.iconUrl ?? launcherFetched.iconUrl,
1077
+ label: launcherUser.label ?? launcherFetched.label,
1078
+ showOnlineDot: launcherUser.showOnlineDot ?? launcherFetched.showOnlineDot ?? false
1079
+ },
1080
+ colorScheme: userConfig.colorScheme ?? DEFAULTS.colorScheme,
1081
+ offset: {
1082
+ bottom: clampInt(offsetUser.bottom, 0, 1e3, DEFAULTS.offsetBottom),
1083
+ side: clampInt(offsetUser.side, 0, 1e3, DEFAULTS.offsetSide)
1084
+ },
1085
+ zIndex: clampInt(userConfig.zIndex, 0, 2e9, DEFAULTS.zIndex),
1086
+ // Defaults to true — the widget shows the attach button unless the
1087
+ // chatbot explicitly opts out via widget_config or the host passes
1088
+ // allowAttachments=false. Server still enforces the same flag at the
1089
+ // upload endpoint either way.
1090
+ allowAttachments: userConfig.allowAttachments ?? fetched?.allowAttachments ?? true
1031
1091
  };
1032
1092
  }
1033
1093
  function sanitizeIncidentBanner(input) {
@@ -1117,9 +1177,40 @@ var CustomerHeroChat = class {
1117
1177
  pendingTriggerId: null,
1118
1178
  pendingPrefill: null,
1119
1179
  incidentBanner: null,
1120
- incidentBannerDismissed: false
1180
+ incidentBannerDismissed: false,
1181
+ readOnly: false
1121
1182
  };
1122
1183
  }
1184
+ /**
1185
+ * Mark the config as loaded and put the client into read-only preview
1186
+ * mode without hitting the API. Used by `@customerhero/react/preview` to
1187
+ * render the widget against a host-supplied config (the dashboard preview
1188
+ * pane). Public API consumers should not call this.
1189
+ *
1190
+ * Pass a config to re-resolve and update the rendered colors/size/launcher
1191
+ * in place. Callers should reuse the same client instance across config
1192
+ * changes so the open animation only fires once.
1193
+ *
1194
+ * @internal
1195
+ */
1196
+ __seedForPreview(config, extras) {
1197
+ const resolved = config ? resolveConfig(config) : this.state.config;
1198
+ if (config) this.userConfig = config;
1199
+ const seededMessages = resolved.welcomeMessage ? [{ role: "bot", content: resolved.welcomeMessage }] : [];
1200
+ const sanitizedBanner = extras && "banner" in extras ? sanitizeIncidentBanner(extras.banner ?? null) : this.state.incidentBanner;
1201
+ this.setState({
1202
+ config: resolved,
1203
+ configLoaded: true,
1204
+ configError: null,
1205
+ readOnly: true,
1206
+ isOpen: true,
1207
+ messages: seededMessages,
1208
+ incidentBanner: sanitizedBanner,
1209
+ // Reset the dismissed flag so toggling the banner on in the dashboard
1210
+ // re-renders it after a previous preview-side dismiss.
1211
+ incidentBannerDismissed: false
1212
+ });
1213
+ }
1123
1214
  // ── Proactive engagement state ─────────────────────────────────────
1124
1215
  triggersRuntime = null;
1125
1216
  preChatFormSubmitted = false;
@@ -1265,6 +1356,7 @@ var CustomerHeroChat = class {
1265
1356
  }
1266
1357
  }
1267
1358
  async sendMessage(message, options) {
1359
+ if (this.state.readOnly) return;
1268
1360
  const trimmed = message.trim();
1269
1361
  const attachmentTokens = options?.attachmentTokens ?? [];
1270
1362
  if (!trimmed || this.state.isLoading) return;
@@ -1817,6 +1909,33 @@ function pickExtension(mime) {
1817
1909
  return "jpg";
1818
1910
  }
1819
1911
 
1912
+ // src/theme.ts
1913
+ function resolveScheme(colorScheme, prefersDark) {
1914
+ if (colorScheme === "dark") return "dark";
1915
+ if (colorScheme === "light") return "light";
1916
+ return prefersDark ? "dark" : "light";
1917
+ }
1918
+ function effectiveColors(config, scheme) {
1919
+ if (scheme === "dark") {
1920
+ return {
1921
+ primary: config.primaryColorDark ?? DEFAULTS.primaryColorDark,
1922
+ background: config.backgroundColorDark ?? DEFAULTS.backgroundColorDark,
1923
+ text: config.textColorDark ?? DEFAULTS.textColorDark
1924
+ };
1925
+ }
1926
+ return {
1927
+ primary: config.primaryColor,
1928
+ background: config.backgroundColor,
1929
+ text: config.textColor
1930
+ };
1931
+ }
1932
+ function sizePreset(size) {
1933
+ return SIZE_PRESETS[size];
1934
+ }
1935
+ function panelRadius(cornerStyle) {
1936
+ return CORNER_RADIUS[cornerStyle];
1937
+ }
1938
+
1820
1939
  // src/screenshot.ts
1821
1940
  var ScreenshotCancelled = class extends Error {
1822
1941
  constructor(message = "Screenshot cancelled") {
@@ -1951,8 +2070,10 @@ async function canvasToBlob(canvas, quality) {
1951
2070
  }
1952
2071
  // Annotate the CommonJS export names for ESM import in node:
1953
2072
  0 && (module.exports = {
2073
+ CORNER_RADIUS,
1954
2074
  CustomerHeroChat,
1955
2075
  DEFAULTS,
2076
+ SIZE_PRESETS,
1956
2077
  SUPPORTED_LOCALES,
1957
2078
  ScreenshotCancelled,
1958
2079
  ScreenshotUnavailable,
@@ -1960,9 +2081,13 @@ async function canvasToBlob(canvas, quality) {
1960
2081
  captureScreenshot,
1961
2082
  createTranslator,
1962
2083
  detectLocale,
2084
+ effectiveColors,
1963
2085
  evaluate,
1964
2086
  isRtlLocale,
2087
+ panelRadius,
1965
2088
  pickFire,
1966
2089
  resolveLocale,
2090
+ resolveScheme,
2091
+ sizePreset,
1967
2092
  startTriggersRuntime
1968
2093
  });
package/dist/index.d.cts CHANGED
@@ -35,6 +35,52 @@ interface CustomerHeroChatConfig {
35
35
  locale?: string;
36
36
  /** Predefined quick-reply options shown before the user sends a message */
37
37
  suggestedMessages?: string[];
38
+ /**
39
+ * Color scheme. `auto` follows the visitor's OS preference; `light` and
40
+ * `dark` force a fixed palette. Defaults to `light` when unset.
41
+ */
42
+ colorScheme?: "auto" | "light" | "dark";
43
+ /** Primary color used in dark mode. Only honoured when the effective
44
+ * scheme resolves to dark; never auto-derived from `primaryColor`. */
45
+ primaryColorDark?: string;
46
+ /** Background color used in dark mode. Only honoured when the effective
47
+ * scheme resolves to dark; never auto-derived from `backgroundColor`. */
48
+ backgroundColorDark?: string;
49
+ /** Text color used in dark mode. Only honoured when the effective scheme
50
+ * resolves to dark; never auto-derived from `textColor`. */
51
+ textColorDark?: string;
52
+ /** Widget size preset. Affects launcher diameter, panel dimensions, and
53
+ * base font size. Defaults to `default`. */
54
+ size?: "compact" | "default" | "large";
55
+ /** Corner radius preset for the chat panel. `soft` ~ small radius,
56
+ * `rounded` ~ medium (default), `square` ~ 0. */
57
+ cornerStyle?: "soft" | "rounded" | "square";
58
+ /** Launcher (floating bubble) customization. Each field is optional. */
59
+ launcher?: {
60
+ /** Custom launcher icon URL. Replaces the default chat-bubble glyph. */
61
+ iconUrl?: string;
62
+ /** Optional CTA label shown next to the launcher (turns the bubble into
63
+ * a pill). Max 60 characters. */
64
+ label?: string;
65
+ /** When true, render a small green dot on the launcher to advertise
66
+ * agent availability. The host site is responsible for keeping
67
+ * business-hours state in sync; the SDK only renders the dot. */
68
+ showOnlineDot?: boolean;
69
+ };
70
+ /** Pixel offsets so the widget can sit above sticky cookie bars or chat
71
+ * CTAs. Each axis defaults to 20 px when unset. Clamped to 0–1000. */
72
+ offset?: {
73
+ bottom?: number;
74
+ side?: number;
75
+ };
76
+ /** Z-index override for sites whose overlays clip the widget. Defaults
77
+ * to 99999. Capped at 2_000_000_000. */
78
+ zIndex?: number;
79
+ /** When false, the input's attach button is hidden so visitors can't
80
+ * even attempt an upload (the server enforces the same flag). Defaults
81
+ * to true. Per-chatbot, sourced from the public widget config endpoint
82
+ * but overrideable on the host. */
83
+ allowAttachments?: boolean;
38
84
  }
39
85
 
40
86
  interface ResolvedConfig {
@@ -51,6 +97,23 @@ interface ResolvedConfig {
51
97
  suggestedMessages: string[];
52
98
  /** Per-chatbot overrides for any translation key, optionally per-locale. */
53
99
  stringOverrides?: StringOverrides;
100
+ colorScheme: "auto" | "light" | "dark";
101
+ primaryColorDark?: string;
102
+ backgroundColorDark?: string;
103
+ textColorDark?: string;
104
+ size: "compact" | "default" | "large";
105
+ cornerStyle: "soft" | "rounded" | "square";
106
+ launcher: {
107
+ iconUrl?: string;
108
+ label?: string;
109
+ showOnlineDot: boolean;
110
+ };
111
+ offset: {
112
+ bottom: number;
113
+ side: number;
114
+ };
115
+ zIndex: number;
116
+ allowAttachments: boolean;
54
117
  }
55
118
  interface MessageSource {
56
119
  index: number;
@@ -303,6 +366,10 @@ interface ChatState {
303
366
  /** True when the visitor has dismissed the active banner this session.
304
367
  * Reset whenever a new banner (different content) lands. */
305
368
  incidentBannerDismissed: boolean;
369
+ /** When true, the chat input is disabled and `sendMessage` is a no-op.
370
+ * Used by the dashboard preview (and any other host that wants a
371
+ * visual-only render). Public API consumers should not set this. */
372
+ readOnly: boolean;
306
373
  }
307
374
 
308
375
  type Listener = (state: ChatState) => void;
@@ -314,6 +381,21 @@ declare class CustomerHeroChat {
314
381
  private identityData;
315
382
  t: TranslateFn;
316
383
  constructor(config: CustomerHeroChatConfig);
384
+ /**
385
+ * Mark the config as loaded and put the client into read-only preview
386
+ * mode without hitting the API. Used by `@customerhero/react/preview` to
387
+ * render the widget against a host-supplied config (the dashboard preview
388
+ * pane). Public API consumers should not call this.
389
+ *
390
+ * Pass a config to re-resolve and update the rendered colors/size/launcher
391
+ * in place. Callers should reuse the same client instance across config
392
+ * changes so the open animation only fires once.
393
+ *
394
+ * @internal
395
+ */
396
+ __seedForPreview(config?: CustomerHeroChatConfig, extras?: {
397
+ banner?: IncidentBanner | null;
398
+ }): void;
317
399
  private triggersRuntime;
318
400
  private preChatFormSubmitted;
319
401
  private readStoredConsent;
@@ -400,7 +482,78 @@ declare const DEFAULTS: {
400
482
  placeholderText: string;
401
483
  welcomeMessage: string;
402
484
  title: string;
485
+ colorScheme: "auto" | "light" | "dark";
486
+ primaryColorDark: string;
487
+ backgroundColorDark: string;
488
+ textColorDark: string;
489
+ size: "compact" | "default" | "large";
490
+ cornerStyle: "soft" | "rounded" | "square";
491
+ offsetBottom: number;
492
+ offsetSide: number;
493
+ zIndex: number;
494
+ };
495
+ declare const SIZE_PRESETS: {
496
+ readonly compact: {
497
+ readonly bubble: 48;
498
+ readonly width: 320;
499
+ readonly height: 480;
500
+ readonly fontSize: 13;
501
+ };
502
+ readonly default: {
503
+ readonly bubble: 56;
504
+ readonly width: 380;
505
+ readonly height: 520;
506
+ readonly fontSize: 14;
507
+ };
508
+ readonly large: {
509
+ readonly bubble: 64;
510
+ readonly width: 440;
511
+ readonly height: 600;
512
+ readonly fontSize: 15;
513
+ };
514
+ };
515
+ declare const CORNER_RADIUS: {
516
+ readonly soft: 8;
517
+ readonly rounded: 16;
518
+ readonly square: 0;
519
+ };
520
+
521
+ type EffectiveScheme = "light" | "dark";
522
+ /**
523
+ * Resolve the configured `colorScheme` against the visitor's OS preference.
524
+ * `prefersDark` is the live value of `matchMedia("(prefers-color-scheme: dark)")`
525
+ * (or `false` when not running in a browser). Kept as a pure function so the
526
+ * caller subscribes once and re-renders.
527
+ */
528
+ declare function resolveScheme(colorScheme: ResolvedConfig["colorScheme"], prefersDark: boolean): EffectiveScheme;
529
+ /**
530
+ * Pick the colors that should drive the widget render right now. When the
531
+ * effective scheme is dark and a dark color is configured, use it; otherwise
532
+ * fall back to the light color (so half-configured dark mode still ships
533
+ * something readable rather than a white panel with white text).
534
+ */
535
+ declare function effectiveColors(config: ResolvedConfig, scheme: EffectiveScheme): {
536
+ primary: string;
537
+ background: string;
538
+ text: string;
539
+ };
540
+ declare function sizePreset(size: ResolvedConfig["size"]): {
541
+ readonly bubble: 48;
542
+ readonly width: 320;
543
+ readonly height: 480;
544
+ readonly fontSize: 13;
545
+ } | {
546
+ readonly bubble: 56;
547
+ readonly width: 380;
548
+ readonly height: 520;
549
+ readonly fontSize: 14;
550
+ } | {
551
+ readonly bubble: 64;
552
+ readonly width: 440;
553
+ readonly height: 600;
554
+ readonly fontSize: 15;
403
555
  };
556
+ declare function panelRadius(cornerStyle: ResolvedConfig["cornerStyle"]): 0 | 8 | 16;
404
557
 
405
558
  declare class ScreenshotCancelled extends Error {
406
559
  constructor(message?: string);
@@ -464,4 +617,4 @@ interface StartTriggersRuntimeOptions {
464
617
  }
465
618
  declare function startTriggersRuntime(options: StartTriggersRuntimeOptions): TriggersRuntimeHandle;
466
619
 
467
- export { type ActionConfirmationBlock, type ChatMessage, type ChatState, type ConsentSettings, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type IdentifyPayload, type IdentityData, type IncidentBanner, type MessageBlock, type MessageRating, type MessageSource, type MessageStatus, type PreChatField, type PreChatFieldKind, type PreChatFormConfig, type PreChatSubmission, type QuickRepliesBlock, type ResolvedConfig, SUPPORTED_LOCALES, ScreenshotCancelled, ScreenshotUnavailable, type StringOverrides, type SupportedLocale, type TranslateFn, type TranslationKey, type Translations, type TriggerAction, type TriggerConditionLeaf, type TriggerConditionNode, type TriggerDefinition, type TriggerFrequency, type TriggersRuntimeHandle, type VisitorContext, canCaptureScreenshot, captureScreenshot, createTranslator, detectLocale, evaluate, isRtlLocale, pickFire, resolveLocale, startTriggersRuntime };
620
+ export { type ActionConfirmationBlock, CORNER_RADIUS, type ChatMessage, type ChatState, type ConsentSettings, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type EffectiveScheme, type IdentifyPayload, type IdentityData, type IncidentBanner, type MessageBlock, type MessageRating, type MessageSource, type MessageStatus, type PreChatField, type PreChatFieldKind, type PreChatFormConfig, type PreChatSubmission, type QuickRepliesBlock, type ResolvedConfig, SIZE_PRESETS, SUPPORTED_LOCALES, ScreenshotCancelled, ScreenshotUnavailable, type StringOverrides, type SupportedLocale, type TranslateFn, type TranslationKey, type Translations, type TriggerAction, type TriggerConditionLeaf, type TriggerConditionNode, type TriggerDefinition, type TriggerFrequency, type TriggersRuntimeHandle, type VisitorContext, canCaptureScreenshot, captureScreenshot, createTranslator, detectLocale, effectiveColors, evaluate, isRtlLocale, panelRadius, pickFire, resolveLocale, resolveScheme, sizePreset, startTriggersRuntime };
package/dist/index.d.ts CHANGED
@@ -35,6 +35,52 @@ interface CustomerHeroChatConfig {
35
35
  locale?: string;
36
36
  /** Predefined quick-reply options shown before the user sends a message */
37
37
  suggestedMessages?: string[];
38
+ /**
39
+ * Color scheme. `auto` follows the visitor's OS preference; `light` and
40
+ * `dark` force a fixed palette. Defaults to `light` when unset.
41
+ */
42
+ colorScheme?: "auto" | "light" | "dark";
43
+ /** Primary color used in dark mode. Only honoured when the effective
44
+ * scheme resolves to dark; never auto-derived from `primaryColor`. */
45
+ primaryColorDark?: string;
46
+ /** Background color used in dark mode. Only honoured when the effective
47
+ * scheme resolves to dark; never auto-derived from `backgroundColor`. */
48
+ backgroundColorDark?: string;
49
+ /** Text color used in dark mode. Only honoured when the effective scheme
50
+ * resolves to dark; never auto-derived from `textColor`. */
51
+ textColorDark?: string;
52
+ /** Widget size preset. Affects launcher diameter, panel dimensions, and
53
+ * base font size. Defaults to `default`. */
54
+ size?: "compact" | "default" | "large";
55
+ /** Corner radius preset for the chat panel. `soft` ~ small radius,
56
+ * `rounded` ~ medium (default), `square` ~ 0. */
57
+ cornerStyle?: "soft" | "rounded" | "square";
58
+ /** Launcher (floating bubble) customization. Each field is optional. */
59
+ launcher?: {
60
+ /** Custom launcher icon URL. Replaces the default chat-bubble glyph. */
61
+ iconUrl?: string;
62
+ /** Optional CTA label shown next to the launcher (turns the bubble into
63
+ * a pill). Max 60 characters. */
64
+ label?: string;
65
+ /** When true, render a small green dot on the launcher to advertise
66
+ * agent availability. The host site is responsible for keeping
67
+ * business-hours state in sync; the SDK only renders the dot. */
68
+ showOnlineDot?: boolean;
69
+ };
70
+ /** Pixel offsets so the widget can sit above sticky cookie bars or chat
71
+ * CTAs. Each axis defaults to 20 px when unset. Clamped to 0–1000. */
72
+ offset?: {
73
+ bottom?: number;
74
+ side?: number;
75
+ };
76
+ /** Z-index override for sites whose overlays clip the widget. Defaults
77
+ * to 99999. Capped at 2_000_000_000. */
78
+ zIndex?: number;
79
+ /** When false, the input's attach button is hidden so visitors can't
80
+ * even attempt an upload (the server enforces the same flag). Defaults
81
+ * to true. Per-chatbot, sourced from the public widget config endpoint
82
+ * but overrideable on the host. */
83
+ allowAttachments?: boolean;
38
84
  }
39
85
 
40
86
  interface ResolvedConfig {
@@ -51,6 +97,23 @@ interface ResolvedConfig {
51
97
  suggestedMessages: string[];
52
98
  /** Per-chatbot overrides for any translation key, optionally per-locale. */
53
99
  stringOverrides?: StringOverrides;
100
+ colorScheme: "auto" | "light" | "dark";
101
+ primaryColorDark?: string;
102
+ backgroundColorDark?: string;
103
+ textColorDark?: string;
104
+ size: "compact" | "default" | "large";
105
+ cornerStyle: "soft" | "rounded" | "square";
106
+ launcher: {
107
+ iconUrl?: string;
108
+ label?: string;
109
+ showOnlineDot: boolean;
110
+ };
111
+ offset: {
112
+ bottom: number;
113
+ side: number;
114
+ };
115
+ zIndex: number;
116
+ allowAttachments: boolean;
54
117
  }
55
118
  interface MessageSource {
56
119
  index: number;
@@ -303,6 +366,10 @@ interface ChatState {
303
366
  /** True when the visitor has dismissed the active banner this session.
304
367
  * Reset whenever a new banner (different content) lands. */
305
368
  incidentBannerDismissed: boolean;
369
+ /** When true, the chat input is disabled and `sendMessage` is a no-op.
370
+ * Used by the dashboard preview (and any other host that wants a
371
+ * visual-only render). Public API consumers should not set this. */
372
+ readOnly: boolean;
306
373
  }
307
374
 
308
375
  type Listener = (state: ChatState) => void;
@@ -314,6 +381,21 @@ declare class CustomerHeroChat {
314
381
  private identityData;
315
382
  t: TranslateFn;
316
383
  constructor(config: CustomerHeroChatConfig);
384
+ /**
385
+ * Mark the config as loaded and put the client into read-only preview
386
+ * mode without hitting the API. Used by `@customerhero/react/preview` to
387
+ * render the widget against a host-supplied config (the dashboard preview
388
+ * pane). Public API consumers should not call this.
389
+ *
390
+ * Pass a config to re-resolve and update the rendered colors/size/launcher
391
+ * in place. Callers should reuse the same client instance across config
392
+ * changes so the open animation only fires once.
393
+ *
394
+ * @internal
395
+ */
396
+ __seedForPreview(config?: CustomerHeroChatConfig, extras?: {
397
+ banner?: IncidentBanner | null;
398
+ }): void;
317
399
  private triggersRuntime;
318
400
  private preChatFormSubmitted;
319
401
  private readStoredConsent;
@@ -400,7 +482,78 @@ declare const DEFAULTS: {
400
482
  placeholderText: string;
401
483
  welcomeMessage: string;
402
484
  title: string;
485
+ colorScheme: "auto" | "light" | "dark";
486
+ primaryColorDark: string;
487
+ backgroundColorDark: string;
488
+ textColorDark: string;
489
+ size: "compact" | "default" | "large";
490
+ cornerStyle: "soft" | "rounded" | "square";
491
+ offsetBottom: number;
492
+ offsetSide: number;
493
+ zIndex: number;
494
+ };
495
+ declare const SIZE_PRESETS: {
496
+ readonly compact: {
497
+ readonly bubble: 48;
498
+ readonly width: 320;
499
+ readonly height: 480;
500
+ readonly fontSize: 13;
501
+ };
502
+ readonly default: {
503
+ readonly bubble: 56;
504
+ readonly width: 380;
505
+ readonly height: 520;
506
+ readonly fontSize: 14;
507
+ };
508
+ readonly large: {
509
+ readonly bubble: 64;
510
+ readonly width: 440;
511
+ readonly height: 600;
512
+ readonly fontSize: 15;
513
+ };
514
+ };
515
+ declare const CORNER_RADIUS: {
516
+ readonly soft: 8;
517
+ readonly rounded: 16;
518
+ readonly square: 0;
519
+ };
520
+
521
+ type EffectiveScheme = "light" | "dark";
522
+ /**
523
+ * Resolve the configured `colorScheme` against the visitor's OS preference.
524
+ * `prefersDark` is the live value of `matchMedia("(prefers-color-scheme: dark)")`
525
+ * (or `false` when not running in a browser). Kept as a pure function so the
526
+ * caller subscribes once and re-renders.
527
+ */
528
+ declare function resolveScheme(colorScheme: ResolvedConfig["colorScheme"], prefersDark: boolean): EffectiveScheme;
529
+ /**
530
+ * Pick the colors that should drive the widget render right now. When the
531
+ * effective scheme is dark and a dark color is configured, use it; otherwise
532
+ * fall back to the light color (so half-configured dark mode still ships
533
+ * something readable rather than a white panel with white text).
534
+ */
535
+ declare function effectiveColors(config: ResolvedConfig, scheme: EffectiveScheme): {
536
+ primary: string;
537
+ background: string;
538
+ text: string;
539
+ };
540
+ declare function sizePreset(size: ResolvedConfig["size"]): {
541
+ readonly bubble: 48;
542
+ readonly width: 320;
543
+ readonly height: 480;
544
+ readonly fontSize: 13;
545
+ } | {
546
+ readonly bubble: 56;
547
+ readonly width: 380;
548
+ readonly height: 520;
549
+ readonly fontSize: 14;
550
+ } | {
551
+ readonly bubble: 64;
552
+ readonly width: 440;
553
+ readonly height: 600;
554
+ readonly fontSize: 15;
403
555
  };
556
+ declare function panelRadius(cornerStyle: ResolvedConfig["cornerStyle"]): 0 | 8 | 16;
404
557
 
405
558
  declare class ScreenshotCancelled extends Error {
406
559
  constructor(message?: string);
@@ -464,4 +617,4 @@ interface StartTriggersRuntimeOptions {
464
617
  }
465
618
  declare function startTriggersRuntime(options: StartTriggersRuntimeOptions): TriggersRuntimeHandle;
466
619
 
467
- export { type ActionConfirmationBlock, type ChatMessage, type ChatState, type ConsentSettings, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type IdentifyPayload, type IdentityData, type IncidentBanner, type MessageBlock, type MessageRating, type MessageSource, type MessageStatus, type PreChatField, type PreChatFieldKind, type PreChatFormConfig, type PreChatSubmission, type QuickRepliesBlock, type ResolvedConfig, SUPPORTED_LOCALES, ScreenshotCancelled, ScreenshotUnavailable, type StringOverrides, type SupportedLocale, type TranslateFn, type TranslationKey, type Translations, type TriggerAction, type TriggerConditionLeaf, type TriggerConditionNode, type TriggerDefinition, type TriggerFrequency, type TriggersRuntimeHandle, type VisitorContext, canCaptureScreenshot, captureScreenshot, createTranslator, detectLocale, evaluate, isRtlLocale, pickFire, resolveLocale, startTriggersRuntime };
620
+ export { type ActionConfirmationBlock, CORNER_RADIUS, type ChatMessage, type ChatState, type ConsentSettings, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type EffectiveScheme, type IdentifyPayload, type IdentityData, type IncidentBanner, type MessageBlock, type MessageRating, type MessageSource, type MessageStatus, type PreChatField, type PreChatFieldKind, type PreChatFormConfig, type PreChatSubmission, type QuickRepliesBlock, type ResolvedConfig, SIZE_PRESETS, SUPPORTED_LOCALES, ScreenshotCancelled, ScreenshotUnavailable, type StringOverrides, type SupportedLocale, type TranslateFn, type TranslationKey, type Translations, type TriggerAction, type TriggerConditionLeaf, type TriggerConditionNode, type TriggerDefinition, type TriggerFrequency, type TriggersRuntimeHandle, type VisitorContext, canCaptureScreenshot, captureScreenshot, createTranslator, detectLocale, effectiveColors, evaluate, isRtlLocale, panelRadius, pickFire, resolveLocale, resolveScheme, sizePreset, startTriggersRuntime };
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
@@ -975,7 +997,14 @@ function startTriggersRuntime(options) {
975
997
  }
976
998
 
977
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
+ }
978
1004
  function resolveConfig(userConfig, fetched) {
1005
+ const launcherUser = userConfig.launcher ?? {};
1006
+ const launcherFetched = fetched?.launcher ?? {};
1007
+ const offsetUser = userConfig.offset ?? {};
979
1008
  return {
980
1009
  chatbotId: userConfig.chatbotId,
981
1010
  apiBase: userConfig.apiBase ?? DEFAULTS.apiBase,
@@ -988,7 +1017,32 @@ function resolveConfig(userConfig, fetched) {
988
1017
  title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
989
1018
  avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
990
1019
  suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? [],
991
- 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
992
1046
  };
993
1047
  }
994
1048
  function sanitizeIncidentBanner(input) {
@@ -1078,9 +1132,40 @@ var CustomerHeroChat = class {
1078
1132
  pendingTriggerId: null,
1079
1133
  pendingPrefill: null,
1080
1134
  incidentBanner: null,
1081
- incidentBannerDismissed: false
1135
+ incidentBannerDismissed: false,
1136
+ readOnly: false
1082
1137
  };
1083
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
+ }
1084
1169
  // ── Proactive engagement state ─────────────────────────────────────
1085
1170
  triggersRuntime = null;
1086
1171
  preChatFormSubmitted = false;
@@ -1226,6 +1311,7 @@ var CustomerHeroChat = class {
1226
1311
  }
1227
1312
  }
1228
1313
  async sendMessage(message, options) {
1314
+ if (this.state.readOnly) return;
1229
1315
  const trimmed = message.trim();
1230
1316
  const attachmentTokens = options?.attachmentTokens ?? [];
1231
1317
  if (!trimmed || this.state.isLoading) return;
@@ -1778,6 +1864,33 @@ function pickExtension(mime) {
1778
1864
  return "jpg";
1779
1865
  }
1780
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
+
1781
1894
  // src/screenshot.ts
1782
1895
  var ScreenshotCancelled = class extends Error {
1783
1896
  constructor(message = "Screenshot cancelled") {
@@ -1911,8 +2024,10 @@ async function canvasToBlob(canvas, quality) {
1911
2024
  });
1912
2025
  }
1913
2026
  export {
2027
+ CORNER_RADIUS,
1914
2028
  CustomerHeroChat,
1915
2029
  DEFAULTS,
2030
+ SIZE_PRESETS,
1916
2031
  SUPPORTED_LOCALES,
1917
2032
  ScreenshotCancelled,
1918
2033
  ScreenshotUnavailable,
@@ -1920,9 +2035,13 @@ export {
1920
2035
  captureScreenshot,
1921
2036
  createTranslator,
1922
2037
  detectLocale,
2038
+ effectiveColors,
1923
2039
  evaluate,
1924
2040
  isRtlLocale,
2041
+ panelRadius,
1925
2042
  pickFire,
1926
2043
  resolveLocale,
2044
+ resolveScheme,
2045
+ sizePreset,
1927
2046
  startTriggersRuntime
1928
2047
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customerhero/js",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "description": "Framework-agnostic JavaScript client for the CustomerHero chat widget.",
6
6
  "keywords": [