@buni.ai/chatbot-core 1.0.11 → 1.0.13

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.esm.js CHANGED
@@ -9,7 +9,8 @@ class BuniChatWidget {
9
9
  };
10
10
  this.eventListeners = new Map();
11
11
  this.widgetElement = null;
12
- this.iframe = null;
12
+ this.triggerIframe = null;
13
+ this.chatIframe = null;
13
14
  this.customerData = null;
14
15
  this.sessionVariables = null;
15
16
  }
@@ -55,160 +56,400 @@ class BuniChatWidget {
55
56
  };
56
57
  const widthValue = ensureUnits(config.width, "350px");
57
58
  const heightValue = ensureUnits(config.height, "650px");
58
- // Check if widget should start minimized
59
- const shouldStartMinimized = config.initialMinimized === true;
59
+ // Configuration
60
60
  const showTriggerText = config.showTriggerText !== false;
61
61
  const hasTriggerText = !!config.triggerText;
62
- // Detect if we're on mobile
62
+ const primaryColor = config.primaryColor || "#795548";
63
+ const triggerText = config.triggerText || "";
64
+ const companyName = config.companyName || "Chat Support";
65
+ // Responsive breakpoints
63
66
  const isMobile = window.innerWidth <= 768;
64
- // Calculate initial dimensions based on minimized state
65
- // On mobile, enforce responsive sizing; on desktop, respect custom dimensions
66
- const initialWidth = shouldStartMinimized
67
- ? showTriggerText && hasTriggerText
68
- ? "auto"
69
- : "52px"
70
- : isMobile
71
- ? "min(calc(100vw - 2rem), 370px)"
72
- : widthValue;
73
- const initialHeight = shouldStartMinimized
74
- ? "52px"
75
- : isMobile
76
- ? "min(calc(100vh - 2rem), 580px)"
77
- : heightValue;
78
- const initialMinWidth = shouldStartMinimized && showTriggerText && hasTriggerText ? "auto" : "";
67
+ // Calculate trigger dimensions
68
+ const triggerWidth = showTriggerText && hasTriggerText ? "auto" : "52px";
69
+ const triggerHeight = "52px";
70
+ const triggerMinWidth = showTriggerText && hasTriggerText ? "auto" : "";
71
+ // Container starts as trigger size
79
72
  container.style.cssText = `
80
73
  position: fixed;
81
74
  pointer-events: none;
82
75
  z-index: 999999;
83
- width: ${initialWidth};
84
- height: ${initialHeight};
85
- ${initialMinWidth ? `min-width: ${initialMinWidth};` : ""}
86
- ${isMobile ? "max-width: 370px; max-height: 580px;" : ""}
87
- transition: all 0.3s ease;
88
- ${this.getPositionStyles(config.position || "bottom-right")}
76
+ width: ${triggerWidth};
77
+ height: ${triggerHeight};
78
+ ${triggerMinWidth ? `min-width: ${triggerMinWidth};` : ""}
79
+ transition: width 0.3s ease, height 0.3s ease, border-radius 0.3s ease;
80
+ ${this.getPositionStyles(config.position || "bottom-right", true)}
89
81
  display: ${config.hideDefaultTrigger ? "none" : "block"};
90
- overflow: ${shouldStartMinimized ? "hidden" : "visible"};
82
+ overflow: hidden;
83
+ box-sizing: border-box;
91
84
  `;
92
- // Create iframe for secure embedding
93
- const iframe = document.createElement("iframe");
94
- // Build URL with configuration parameters
95
- const params = new URLSearchParams({
96
- token: this.options.token,
97
- embedded: "true", // Enable postMessage communication
98
- source: "package",
99
- framework: this.options.framework || "vanilla",
100
- });
101
- // Add string parameters only if provided
102
- if (config.theme)
103
- params.set("theme", config.theme);
104
- if (config.primaryColor)
105
- params.set("primaryColor", config.primaryColor);
106
- if (config.secondaryColor)
107
- params.set("secondaryColor", config.secondaryColor);
108
- if (config.position)
109
- params.set("position", config.position);
110
- if (widthValue && config.width !== undefined)
111
- params.set("width", widthValue);
112
- if (heightValue && config.height !== undefined)
113
- params.set("height", heightValue);
114
- if (config.customAvatar)
115
- params.set("customAvatar", config.customAvatar);
116
- if (config.companyName)
117
- params.set("companyName", config.companyName);
118
- if (config.welcomeMessage)
119
- params.set("welcomeMessage", config.welcomeMessage);
120
- if (config.triggerText)
121
- params.set("triggerText", config.triggerText);
122
- if (config.borderRadius)
123
- params.set("borderRadius", config.borderRadius);
124
- if (config.avatarType)
125
- params.set("avatarType", config.avatarType);
126
- if (config.avatarText)
127
- params.set("avatarText", config.avatarText);
128
- // Boolean options - only pass if explicitly defined
129
- if (config.showBranding !== undefined) {
130
- params.set("showBranding", String(config.showBranding));
131
- }
132
- if (config.autoOpen !== undefined) {
133
- params.set("autoOpen", String(config.autoOpen));
134
- }
135
- if (config.minimized !== undefined) {
136
- params.set("minimized", String(config.minimized));
137
- }
138
- if (config.initialMinimized !== undefined) {
139
- params.set("initialMinimized", String(config.initialMinimized));
140
- }
141
- if (config.allowMinimize !== undefined) {
142
- params.set("allowMinimize", String(config.allowMinimize));
143
- }
144
- if (config.showMinimize !== undefined) {
145
- params.set("showMinimize", String(config.showMinimize));
146
- }
147
- if (config.allowClose !== undefined) {
148
- params.set("allowClose", String(config.allowClose));
149
- }
150
- if (config.enableFileUpload !== undefined) {
151
- params.set("enableFileUpload", String(config.enableFileUpload));
152
- }
153
- if (config.showTimestamps !== undefined) {
154
- params.set("showTimestamps", String(config.showTimestamps));
155
- }
156
- if (config.showTriggerText !== undefined) {
157
- params.set("showTriggerText", String(config.showTriggerText));
158
- }
159
- if (config.hideDefaultTrigger !== undefined) {
160
- params.set("hideDefaultTrigger", String(config.hideDefaultTrigger));
161
- }
162
- if (config.enableMobile !== undefined) {
163
- params.set("enableMobile", String(config.enableMobile));
164
- }
165
- // Pre-chat form and start button - only pass if explicitly defined
166
- if (config.showPreChatForm !== undefined) {
167
- params.set("showPreChatForm", String(config.showPreChatForm));
168
- }
169
- if (config.showStartButton !== undefined) {
170
- params.set("showStartButton", String(config.showStartButton));
171
- }
172
- if (config.startButtonText) {
173
- params.set("startButtonText", config.startButtonText);
174
- }
175
- if (config.preChatFormFields) {
176
- params.set("preChatFormFields", JSON.stringify(config.preChatFormFields));
177
- }
178
- iframe.src = `${this.getBaseUrl()}/embed/chat?${params.toString()}`;
179
- // Set styles individually - iframe fills container 100%
180
- iframe.style.position = "absolute";
181
- iframe.style.top = "0";
182
- iframe.style.left = "0";
183
- iframe.style.border = "none";
184
- iframe.style.width = "100%";
185
- iframe.style.height = "100%";
186
- // Use circular border for icon-only minimized, rounded for text button or expanded
187
- iframe.style.borderRadius =
188
- shouldStartMinimized && initialWidth === "52px" ? "50%" : "12px";
189
- iframe.style.transition = "all 0.3s ease";
190
- iframe.style.pointerEvents = "auto";
191
- iframe.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
192
- iframe.setAttribute("allow", "clipboard-write");
193
- iframe.setAttribute("title", "BuniAI Chat Widget");
194
- iframe.onload = () => {
85
+ // Create inline trigger iframe (no src)
86
+ const triggerIframe = document.createElement("iframe");
87
+ triggerIframe.id = "buni-trigger-iframe";
88
+ triggerIframe.style.cssText = `
89
+ position: absolute;
90
+ top: 0;
91
+ left: 0;
92
+ border: none;
93
+ width: 100%;
94
+ height: 100%;
95
+ box-sizing: border-box;
96
+ border-radius: ${showTriggerText && hasTriggerText ? "26px" : "50%"};
97
+ transition: border-radius 0.3s ease, box-shadow 0.3s ease;
98
+ pointer-events: auto;
99
+ `;
100
+ triggerIframe.setAttribute("title", "BuniAI Chat Trigger");
101
+ // Inject trigger HTML content directly (no src)
102
+ container.appendChild(triggerIframe);
103
+ document.body.appendChild(container);
104
+ triggerIframe.onload = () => {
105
+ var _a;
106
+ const triggerDoc = triggerIframe.contentDocument ||
107
+ ((_a = triggerIframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document);
108
+ if (!triggerDoc) {
109
+ reject(new Error("Failed to access trigger iframe document"));
110
+ return;
111
+ }
112
+ // Build inline HTML for trigger
113
+ const triggerHTML = this.buildTriggerHTML(primaryColor, triggerText || companyName, showTriggerText && hasTriggerText, config.customAvatar, config.avatarType, config.avatarText);
114
+ triggerDoc.open();
115
+ triggerDoc.write(triggerHTML);
116
+ triggerDoc.close();
117
+ // Create chat iframe (initially hidden)
118
+ const chatIframe = document.createElement("iframe");
119
+ chatIframe.id = "buni-chat-iframe";
120
+ // Build URL with configuration parameters
121
+ const params = new URLSearchParams({
122
+ token: this.options.token,
123
+ embedded: "true",
124
+ source: "package",
125
+ framework: this.options.framework || "vanilla",
126
+ });
127
+ // Add all configuration parameters
128
+ if (config.theme)
129
+ params.set("theme", config.theme);
130
+ if (config.primaryColor)
131
+ params.set("primaryColor", config.primaryColor);
132
+ if (config.secondaryColor)
133
+ params.set("secondaryColor", config.secondaryColor);
134
+ if (config.position)
135
+ params.set("position", config.position);
136
+ if (widthValue && config.width !== undefined)
137
+ params.set("width", widthValue);
138
+ if (heightValue && config.height !== undefined)
139
+ params.set("height", heightValue);
140
+ if (config.customAvatar)
141
+ params.set("customAvatar", config.customAvatar);
142
+ if (config.companyName)
143
+ params.set("companyName", config.companyName);
144
+ if (config.welcomeMessage)
145
+ params.set("welcomeMessage", config.welcomeMessage);
146
+ if (config.triggerText)
147
+ params.set("triggerText", config.triggerText);
148
+ if (config.borderRadius)
149
+ params.set("borderRadius", config.borderRadius);
150
+ if (config.avatarType)
151
+ params.set("avatarType", config.avatarType);
152
+ if (config.avatarText)
153
+ params.set("avatarText", config.avatarText);
154
+ // Boolean options
155
+ if (config.showBranding !== undefined)
156
+ params.set("showBranding", String(config.showBranding));
157
+ if (config.autoOpen !== undefined)
158
+ params.set("autoOpen", String(config.autoOpen));
159
+ if (config.allowMinimize !== undefined)
160
+ params.set("allowMinimize", String(config.allowMinimize));
161
+ if (config.showMinimize !== undefined)
162
+ params.set("showMinimize", String(config.showMinimize));
163
+ if (config.allowClose !== undefined)
164
+ params.set("allowClose", String(config.allowClose));
165
+ if (config.enableFileUpload !== undefined)
166
+ params.set("enableFileUpload", String(config.enableFileUpload));
167
+ if (config.showTimestamps !== undefined)
168
+ params.set("showTimestamps", String(config.showTimestamps));
169
+ if (config.enableMobile !== undefined)
170
+ params.set("enableMobile", String(config.enableMobile));
171
+ if (config.showPreChatForm !== undefined)
172
+ params.set("showPreChatForm", String(config.showPreChatForm));
173
+ if (config.showStartButton !== undefined)
174
+ params.set("showStartButton", String(config.showStartButton));
175
+ if (config.startButtonText)
176
+ params.set("startButtonText", config.startButtonText);
177
+ if (config.preChatFormFields)
178
+ params.set("preChatFormFields", JSON.stringify(config.preChatFormFields));
179
+ chatIframe.src = `${this.getBaseUrl()}/embed/chat?${params.toString()}`;
180
+ // Chat iframe styling - initially hidden
181
+ chatIframe.style.cssText = `
182
+ position: absolute;
183
+ top: 0;
184
+ left: 0;
185
+ border: none;
186
+ width: 100%;
187
+ height: 100%;
188
+ box-sizing: border-box;
189
+ border-radius: ${isMobile ? "0" : "16px"};
190
+ transition: border-radius 0.3s ease, box-shadow 0.3s ease;
191
+ pointer-events: auto;
192
+ box-shadow: ${isMobile ? "none" : "0 8px 32px rgba(0,0,0,0.15)"};
193
+ display: none;
194
+ `;
195
+ chatIframe.setAttribute("allow", "clipboard-write");
196
+ chatIframe.setAttribute("title", "BuniAI Chat Widget");
197
+ container.appendChild(chatIframe);
198
+ chatIframe.onload = () => {
199
+ // Set visibility hidden after iframe loads to allow content initialization
200
+ chatIframe.style.visibility = "hidden";
201
+ this.setupPostMessageAPI(chatIframe);
202
+ };
195
203
  this.widgetElement = container;
196
- this.iframe = iframe;
197
- this.state.isMinimized = shouldStartMinimized;
198
- this.setupPostMessageAPI(iframe);
204
+ this.triggerIframe = triggerIframe;
205
+ this.chatIframe = chatIframe;
206
+ this.state.isMinimized = true;
207
+ this.state.isLoaded = true;
199
208
  resolve();
200
209
  };
201
- iframe.onerror = () => {
202
- reject(new Error("Failed to load BuniAI widget iframe"));
203
- };
204
- container.appendChild(iframe);
205
- document.body.appendChild(container);
210
+ // Trigger the load event
211
+ triggerIframe.src = "about:blank";
212
+ // Set up communication with trigger iframe
213
+ window.addEventListener("message", (event) => {
214
+ // Check if message is from trigger iframe
215
+ if (event.data.type === "trigger_clicked" &&
216
+ event.source === triggerIframe.contentWindow) {
217
+ this.openChat();
218
+ }
219
+ });
206
220
  });
207
221
  }
208
- getPositionStyles(position) {
209
- // Use smaller margins on mobile for better space utilization
222
+ buildTriggerHTML(primaryColor, text, showText, customAvatar, avatarType, avatarText) {
223
+ const avatarContent = customAvatar
224
+ ? `<img src="${customAvatar}" alt="Avatar" style="width: 32px; height: 32px; border-radius: 50%;" />`
225
+ : avatarType === "text" && avatarText
226
+ ? `<div style="width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.3); display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: white;">${avatarText.substring(0, 2).toUpperCase()}</div>`
227
+ : `<svg width="28" height="28" viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1 0-.9-2-2zm0 14H6l-2 2V4h16v12z"/></svg>`;
228
+ return `<!DOCTYPE html>
229
+ <html>
230
+ <head>
231
+ <meta charset="UTF-8">
232
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
233
+ <style>
234
+ * { margin: 0; padding: 0; box-sizing: border-box; }
235
+ body {
236
+ width: 100%;
237
+ height: 100%;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
242
+ overflow: hidden;
243
+ }
244
+ .trigger {
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ gap: ${showText ? "12px" : "0"};
249
+ padding: ${showText ? "10px 20px" : "10px"};
250
+ background: ${primaryColor};
251
+ color: white;
252
+ border-radius: ${showText ? "26px" : "50%"};
253
+ cursor: pointer;
254
+ transition: all 0.3s ease;
255
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15);
256
+ animation: pulse 2s infinite;
257
+ ${showText ? "" : "width: 52px; height: 52px;"}
258
+ }
259
+ .trigger:hover {
260
+ transform: translateY(-2px);
261
+ filter: brightness(1.1);
262
+ box-shadow: 0 6px 24px ${primaryColor}99;
263
+ }
264
+ .trigger:active {
265
+ transform: translateY(0);
266
+ }
267
+ .avatar {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: center;
271
+ flex-shrink: 0;
272
+ }
273
+ .text {
274
+ font-size: 14px;
275
+ font-weight: 600;
276
+ white-space: nowrap;
277
+ }
278
+ .badge {
279
+ position: absolute;
280
+ top: -4px;
281
+ right: -4px;
282
+ background: #f44336;
283
+ color: white;
284
+ border-radius: 10px;
285
+ padding: 2px 6px;
286
+ font-size: 11px;
287
+ font-weight: 600;
288
+ min-width: 18px;
289
+ text-align: center;
290
+ display: none;
291
+ animation: pulse 2s infinite;
292
+ }
293
+ .badge.show {
294
+ display: block;
295
+ }
296
+ @keyframes pulse {
297
+ 0%, 100% {
298
+ transform: scale(1);
299
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15);
300
+ }
301
+ 50% {
302
+ transform: scale(1.02);
303
+ box-shadow: 0 6px 24px ${primaryColor}99;
304
+ }
305
+ }
306
+ </style>
307
+ </head>
308
+ <body>
309
+ <div style="position: relative;">
310
+ <div class="trigger" onclick="handleClick()">
311
+ <div class="avatar">${avatarContent}</div>
312
+ ${showText ? `<span class="text">${text}</span>` : ""}
313
+ </div>
314
+ <div class="badge" id="badge">0</div>
315
+ </div>
316
+ <script>
317
+ function handleClick() {
318
+ window.parent.postMessage({ type: 'trigger_clicked' }, '*');
319
+ }
320
+
321
+ // Listen for unread count updates
322
+ window.addEventListener('message', function(event) {
323
+ if (event.data.type === 'updateUnreadCount') {
324
+ const badge = document.getElementById('badge');
325
+ const count = event.data.count || 0;
326
+ badge.textContent = count;
327
+ if (count > 0) {
328
+ badge.classList.add('show');
329
+ } else {
330
+ badge.classList.remove('show');
331
+ }
332
+ }
333
+ });
334
+ </script>
335
+ </body>
336
+ </html>`;
337
+ }
338
+ async openChat() {
339
+ if (!this.chatIframe || !this.widgetElement || !this.triggerIframe)
340
+ return;
341
+ if (this.state.isOpen)
342
+ return; // Already open
343
+ const config = this.options.config || {};
210
344
  const isMobile = window.innerWidth <= 768;
211
- const margin = isMobile ? "8px" : "20px";
345
+ const isTablet = window.innerWidth > 768 && window.innerWidth <= 1024;
346
+ const ensureUnits = (value, defaultValue) => {
347
+ if (!value)
348
+ return defaultValue;
349
+ const str = String(value);
350
+ if (str.match(/^[\d.]+\s*(px|em|rem|%|vh|vw)$/))
351
+ return str;
352
+ if (str.match(/^[\d.]+$/))
353
+ return `${str}px`;
354
+ return str;
355
+ };
356
+ const widthValue = ensureUnits(config.width, "350px");
357
+ const heightValue = ensureUnits(config.height, "650px");
358
+ // Calculate chat dimensions
359
+ const chatWidth = isMobile
360
+ ? "100vw"
361
+ : isTablet
362
+ ? "min(calc(100vw - 3rem), 370px)"
363
+ : widthValue;
364
+ const chatHeight = isMobile
365
+ ? "100vh"
366
+ : isTablet
367
+ ? "min(calc(100vh - 3rem), 620px)"
368
+ : heightValue;
369
+ // Resize container for chat
370
+ this.widgetElement.style.cssText = `
371
+ position: fixed;
372
+ pointer-events: none;
373
+ z-index: 999999;
374
+ width: ${chatWidth};
375
+ height: ${chatHeight};
376
+ ${isMobile ? "max-width: 100vw; max-height: 100vh;" : ""}
377
+ transition: width 0.3s ease, height 0.3s ease, border-radius 0.3s ease;
378
+ ${this.getPositionStyles(config.position || "bottom-right", false)}
379
+ display: block;
380
+ overflow: visible;
381
+ box-sizing: border-box;
382
+ `;
383
+ // Hide trigger, show chat
384
+ this.triggerIframe.style.display = "none";
385
+ this.chatIframe.style.display = "block";
386
+ this.chatIframe.style.visibility = "visible";
387
+ this.state.isMinimized = false;
388
+ this.state.isOpen = true;
389
+ this.emit("maximized", { timestamp: Date.now() });
390
+ }
391
+ closeChat() {
392
+ if (!this.chatIframe || !this.widgetElement || !this.triggerIframe)
393
+ return;
394
+ const config = this.options.config || {};
395
+ const showTriggerText = config.showTriggerText !== false;
396
+ const hasTriggerText = !!config.triggerText;
397
+ // Hide chat iframe
398
+ this.chatIframe.style.display = "none";
399
+ this.chatIframe.style.visibility = "hidden";
400
+ // Resize container back to trigger size
401
+ const triggerWidth = showTriggerText && hasTriggerText ? "auto" : "52px";
402
+ const triggerHeight = "52px";
403
+ const triggerMinWidth = showTriggerText && hasTriggerText ? "auto" : "";
404
+ this.widgetElement.style.cssText = `
405
+ position: fixed;
406
+ pointer-events: none;
407
+ z-index: 999999;
408
+ width: ${triggerWidth};
409
+ height: ${triggerHeight};
410
+ ${triggerMinWidth ? `min-width: ${triggerMinWidth};` : ""}
411
+ transition: width 0.3s ease, height 0.3s ease, border-radius 0.3s ease;
412
+ ${this.getPositionStyles(config.position || "bottom-right", true)}
413
+ display: block;
414
+ overflow: hidden;
415
+ box-sizing: border-box;
416
+ `;
417
+ // Show trigger again
418
+ this.triggerIframe.style.display = "block";
419
+ this.state.isMinimized = true;
420
+ this.state.isOpen = false;
421
+ this.emit("minimized", { timestamp: Date.now() });
422
+ }
423
+ getPositionStyles(position, isMinimized = false) {
424
+ // Responsive positioning following mobile-first best practices
425
+ const viewportWidth = window.innerWidth;
426
+ const isMobile = viewportWidth <= 768;
427
+ const isTablet = viewportWidth > 768 && viewportWidth <= 1024;
428
+ // For extra small devices (Galaxy S8+, iPhone SE, etc.), use minimal or no margins
429
+ // For mobile, use small margins for breathing room
430
+ // For tablet/desktop, use larger margins for floating appearance
431
+ let margin;
432
+ if (isMinimized) {
433
+ // Minimized button always needs some space from edges
434
+ margin = isMobile ? "12px" : "20px";
435
+ }
436
+ else if (isMobile) {
437
+ // Full screen on all mobile devices (no margins)
438
+ margin = "0";
439
+ }
440
+ else if (isTablet) {
441
+ // Medium margins on tablets
442
+ margin = "16px";
443
+ }
444
+ else {
445
+ // Larger margins on desktop for floating effect
446
+ margin = "24px";
447
+ }
448
+ // On mobile screens when not minimized, position at edges for full coverage
449
+ if (isMobile && !isMinimized) {
450
+ return `top: 0; left: 0; right: 0; bottom: 0;`;
451
+ }
452
+ // Standard positioning for larger screens or minimized state
212
453
  switch (position) {
213
454
  case "bottom-right":
214
455
  return `bottom: ${margin}; right: ${margin};`;
@@ -225,7 +466,7 @@ class BuniChatWidget {
225
466
  setupPostMessageAPI(iframe) {
226
467
  // Listen for messages from the iframe
227
468
  window.addEventListener("message", (event) => {
228
- var _a, _b;
469
+ var _a;
229
470
  // Verify origin for security
230
471
  if (event.origin !== this.getBaseUrl()) {
231
472
  return;
@@ -256,60 +497,15 @@ class BuniChatWidget {
256
497
  }
257
498
  break;
258
499
  case "minimized":
259
- this.state.isMinimized = true;
260
- if (this.widgetElement && data.dimensions) {
261
- // Resize container to trigger button size when minimized
262
- this.widgetElement.style.width = data.dimensions.width;
263
- this.widgetElement.style.height = data.dimensions.height;
264
- this.widgetElement.style.overflow = "hidden";
265
- if (data.dimensions.minWidth) {
266
- this.widgetElement.style.minWidth = data.dimensions.minWidth;
267
- }
268
- }
269
- if (this.iframe) {
270
- // Use circular border for icon-only, rounded for text button
271
- this.iframe.style.borderRadius =
272
- ((_a = data.dimensions) === null || _a === void 0 ? void 0 : _a.width) === ((_b = data.dimensions) === null || _b === void 0 ? void 0 : _b.height)
273
- ? "50%"
274
- : "26px";
275
- }
276
- this.emit("minimized", data);
500
+ // User clicked minimize in chat - close chat and show trigger
501
+ this.closeChat();
277
502
  break;
278
- case "maximized":
279
- this.state.isMinimized = false;
280
- if (this.widgetElement) {
281
- // Check if we're on mobile
282
- const isMobile = window.innerWidth <= 768;
283
- const config = this.options.config || {};
284
- if (isMobile) {
285
- // On mobile, enforce responsive sizing
286
- this.widgetElement.style.width = "min(calc(100vw - 2rem), 370px)";
287
- this.widgetElement.style.height =
288
- "min(calc(100vh - 2rem), 680px)";
289
- this.widgetElement.style.maxWidth = "370px";
290
- this.widgetElement.style.maxHeight = "680px";
291
- }
292
- else {
293
- // On desktop, respect custom dimensions
294
- const ensureUnits = (value, defaultValue) => {
295
- if (!value)
296
- return defaultValue;
297
- const str = String(value);
298
- if (str.match(/^[\d.]+\s*(px|em|rem|%|vh|vw)$/))
299
- return str;
300
- if (str.match(/^[\d.]+$/))
301
- return `${str}px`;
302
- return str;
303
- };
304
- this.widgetElement.style.width = ensureUnits(config.width, "350px");
305
- this.widgetElement.style.height = ensureUnits(config.height, "650px");
306
- }
307
- this.widgetElement.style.overflow = "visible";
308
- }
309
- if (this.iframe) {
310
- this.iframe.style.borderRadius = "12px";
503
+ case "new_unread_message":
504
+ // Update unread count in trigger
505
+ this.state.unreadCount++;
506
+ if ((_a = this.triggerIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
507
+ this.triggerIframe.contentWindow.postMessage({ type: "updateUnreadCount", count: this.state.unreadCount }, "*");
311
508
  }
312
- this.emit("maximized", data);
313
509
  break;
314
510
  case "customer_data_updated":
315
511
  this.customerData = data;
@@ -330,8 +526,8 @@ class BuniChatWidget {
330
526
  }
331
527
  postMessageToWidget(type, data) {
332
528
  var _a;
333
- if (this.widgetElement && this.widgetElement instanceof HTMLIFrameElement) {
334
- (_a = this.widgetElement.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage({ type, data }, this.getBaseUrl());
529
+ if ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
530
+ this.chatIframe.contentWindow.postMessage({ type, data }, this.getBaseUrl());
335
531
  }
336
532
  }
337
533
  getBaseUrl() {
@@ -344,44 +540,49 @@ class BuniChatWidget {
344
540
  this.widgetElement.remove();
345
541
  this.widgetElement = null;
346
542
  }
543
+ this.triggerIframe = null;
544
+ this.chatIframe = null;
347
545
  this.eventListeners.clear();
348
546
  this.state.isLoaded = false;
349
547
  this.customerData = null;
350
548
  this.sessionVariables = null;
351
549
  }
352
550
  show() {
353
- // Show the iframe if it was hidden (hideDefaultTrigger mode)
354
- if (this.widgetElement instanceof HTMLIFrameElement) {
551
+ // Show the widget container if it was hidden (hideDefaultTrigger mode)
552
+ if (this.widgetElement) {
355
553
  this.widgetElement.style.display = "block";
356
554
  }
357
- this.postMessageToWidget("show");
358
- this.state.isOpen = true;
359
- this.state.unreadCount = 0;
360
- this.emit("visibility_changed", { visibility: "visible" });
555
+ // Open chat if not already open
556
+ if (!this.chatIframe && !this.state.isOpen) {
557
+ this.openChat();
558
+ }
361
559
  }
362
560
  hide() {
363
561
  var _a;
364
- this.postMessageToWidget("hide");
365
- // If hideDefaultTrigger is enabled, completely hide the iframe
366
- if (((_a = this.options.config) === null || _a === void 0 ? void 0 : _a.hideDefaultTrigger) &&
367
- this.widgetElement instanceof HTMLIFrameElement) {
562
+ // Close chat if open
563
+ if (this.chatIframe) {
564
+ this.closeChat();
565
+ }
566
+ // If hideDefaultTrigger is enabled, completely hide the container
567
+ if (((_a = this.options.config) === null || _a === void 0 ? void 0 : _a.hideDefaultTrigger) && this.widgetElement) {
368
568
  this.widgetElement.style.display = "none";
369
569
  }
370
570
  this.state.isOpen = false;
371
571
  this.emit("visibility_changed", { visibility: "hidden" });
372
572
  }
373
573
  toggle() {
374
- this.state.isOpen ? this.hide() : this.show();
574
+ if (this.chatIframe || this.state.isOpen) {
575
+ this.closeChat();
576
+ }
577
+ else {
578
+ this.openChat();
579
+ }
375
580
  }
376
581
  minimize() {
377
- this.postMessageToWidget("minimize");
378
- this.state.isMinimized = true;
379
- this.emit("minimized", { timestamp: Date.now() });
582
+ this.closeChat();
380
583
  }
381
584
  maximize() {
382
- this.postMessageToWidget("maximize");
383
- this.state.isMinimized = false;
384
- this.emit("maximized", { timestamp: Date.now() });
585
+ this.openChat();
385
586
  }
386
587
  setCustomerData(data) {
387
588
  this.customerData = { ...this.customerData, ...data };