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