@buni.ai/chatbot-angular 1.0.15 → 1.0.17

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