@buni.ai/chatbot-core 1.0.12 → 1.0.14

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