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