aumera-on-screen-widget 0.0.19 → 0.0.21

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.
Files changed (2) hide show
  1. package/df-btn.js +272 -67
  2. package/package.json +1 -1
package/df-btn.js CHANGED
@@ -31,7 +31,7 @@
31
31
  const transform = `w_${width},h_${height},c_fill,f_auto,q_auto`;
32
32
 
33
33
  // Insert transformation right after `/upload/`
34
- return url.replace('/upload/', `/upload/${transform}/`);
34
+ return url.replace("/upload/", `/upload/${transform}/`);
35
35
  }
36
36
 
37
37
  // Async function to fetch chatbot settings from GraphQL
@@ -45,7 +45,7 @@
45
45
  "env:",
46
46
  env,
47
47
  "language:",
48
- language
48
+ language,
49
49
  );
50
50
 
51
51
  const query = `
@@ -81,14 +81,14 @@
81
81
  console.log(
82
82
  "[OSW Widget] Response status:",
83
83
  response.status,
84
- response.statusText
84
+ response.statusText,
85
85
  );
86
86
 
87
87
  if (!response.ok) {
88
88
  console.error(
89
89
  "[OSW Widget] Failed to fetch - Status:",
90
90
  response.status,
91
- response.statusText
91
+ response.statusText,
92
92
  );
93
93
  const errorText = await response
94
94
  .text()
@@ -103,13 +103,13 @@
103
103
  if (result.data?.oswChatbotSettingsPublic?.success) {
104
104
  console.log(
105
105
  "[OSW Widget] Settings retrieved successfully:",
106
- result.data.oswChatbotSettingsPublic.settings
106
+ result.data.oswChatbotSettingsPublic.settings,
107
107
  );
108
108
  return result.data.oswChatbotSettingsPublic.settings;
109
109
  }
110
110
 
111
111
  console.warn(
112
- "[OSW Widget] GraphQL query succeeded but success=false or missing data"
112
+ "[OSW Widget] GraphQL query succeeded but success=false or missing data",
113
113
  );
114
114
  return null;
115
115
  } catch (error) {
@@ -119,6 +119,17 @@
119
119
  }
120
120
  }
121
121
 
122
+ // Helper function to determine if a color is dark based on luminance
123
+ function isColorDark(hexColor) {
124
+ if (!hexColor) return false;
125
+ const hex = hexColor.replace("#", "");
126
+ const r = parseInt(hex.substr(0, 2), 16) / 255;
127
+ const g = parseInt(hex.substr(2, 2), 16) / 255;
128
+ const b = parseInt(hex.substr(4, 2), 16) / 255;
129
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
130
+ return luminance < 0.5;
131
+ }
132
+
122
133
  // Default configuration with fallback values
123
134
  const defaultConfig = {
124
135
  env: "prod",
@@ -189,10 +200,10 @@
189
200
  // Fetch settings from GraphQL and merge with config
190
201
  if (!config.orgId) {
191
202
  console.error(
192
- "[OSW Widget] No organization ID provided! Widget will not be displayed."
203
+ "[OSW Widget] No organization ID provided! Widget will not be displayed.",
193
204
  );
194
205
  console.error(
195
- "[OSW Widget] Add orgId attribute to the script tag: <script id='df-btn' orgId='YOUR_ORG_ID' ...>"
206
+ "[OSW Widget] Add orgId attribute to the script tag: <script id='df-btn' orgId='YOUR_ORG_ID' ...>",
196
207
  );
197
208
  return; // Exit if no orgId
198
209
  }
@@ -200,12 +211,12 @@
200
211
  const settings = await fetchChatbotSettings(
201
212
  config.orgId,
202
213
  config.env,
203
- detectedLang
214
+ detectedLang,
204
215
  );
205
216
 
206
217
  if (!settings) {
207
218
  console.warn(
208
- "[OSW Widget] Failed to fetch chatbot settings. Using fallback configuration."
219
+ "[OSW Widget] Failed to fetch chatbot settings. Using fallback configuration.",
209
220
  );
210
221
  console.warn("[OSW Widget] Check:");
211
222
  console.warn("[OSW Widget] 1. Network tab for CORS errors");
@@ -224,7 +235,8 @@
224
235
  config.backgroundDark =
225
236
  settings.oswColourBackgroundDark || config.backgroundDark;
226
237
  // Update hasDarkMode if GraphQL provides dark background
227
- config.hasDarkMode = config.hasDarkMode || !!settings.oswColourBackgroundDark;
238
+ config.hasDarkMode =
239
+ config.hasDarkMode || !!settings.oswColourBackgroundDark;
228
240
  config.fontColor = settings.oswColourFontLight || config.fontColor;
229
241
  config.fontColorDark = settings.oswColourFontDark || config.fontColorDark;
230
242
  config.showNotification =
@@ -237,7 +249,7 @@
237
249
  config.logoDark = cloudinaryResize(settings.chatbotMask, 64, 64);
238
250
  console.log(
239
251
  "[OSW Widget] Using chatbotMask as logo:",
240
- settings.chatbotMask
252
+ settings.chatbotMask,
241
253
  );
242
254
  }
243
255
  }
@@ -350,6 +362,18 @@
350
362
  height: 48px;
351
363
  }
352
364
 
365
+ /* Make header transparent when chat is open */
366
+ .df-btn:not(.df-closed) .df-btn-header {
367
+ background: transparent;
368
+ position: absolute;
369
+ top: 0;
370
+ left: 0;
371
+ right: 0;
372
+ z-index: 10001;
373
+ padding: 12px;
374
+ box-sizing: border-box;
375
+ }
376
+
353
377
  .df-btn-text:before {
354
378
  background-position: center;
355
379
  background-repeat: no-repeat;
@@ -371,11 +395,12 @@
371
395
  }
372
396
 
373
397
  .df-btn:not(.df-closed){
374
- border-radius: 16px
398
+ border-radius: 16px;
399
+ overflow: hidden;
375
400
  }
376
401
 
377
- .df-btn:not(.df-closed) > .df-btn-text:before {
378
- background-image: url('${origin}/assets/close.svg')
402
+ .df-btn:not(.df-closed) .df-btn-text:before {
403
+ display: none;
379
404
  }
380
405
 
381
406
  .df-btn-content {
@@ -392,6 +417,8 @@
392
417
  ${config.position === "left" ? "float: left;" : "float: right;"}
393
418
  opacity: 1;
394
419
  border-radius: 0 0 16px 16px;
420
+ /* iOS Safari keyboard fix: Explicit background to prevent transparency bleed */
421
+ background-color: ${config.background || "#FEFFFF"};
395
422
  }
396
423
 
397
424
  .df-btn:not(.df-closed) > .df-btn-content {
@@ -406,8 +433,8 @@
406
433
  }
407
434
 
408
435
  .maximize-minimize-btn {
409
- width: 16px;
410
- height: 16px;
436
+ width: auto;
437
+ height: auto;
411
438
  cursor: pointer;
412
439
  margin-right: 12px;
413
440
  display: flex;
@@ -425,16 +452,12 @@
425
452
  bottom: 0 !important;
426
453
  margin: 0 !important;
427
454
  border-radius: 0 !important;
428
- /* Handle safe areas on mobile devices */
429
- padding-bottom: env(safe-area-inset-bottom, 0);
430
455
  }
431
456
  .df-btn.df-maximized .df-btn-content {
432
457
  width: 100vw !important;
433
458
  max-width: 100vw !important;
434
- height: calc(100vh - 56px) !important;
435
- height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important; /* Account for safe area */
436
- max-height: calc(100vh - 56px) !important;
437
- max-height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
459
+ height: 100dvh !important;
460
+ max-height: 100dvh !important;
438
461
  opacity: 1 !important;
439
462
  float: none !important;
440
463
  display: block !important;
@@ -446,8 +469,8 @@
446
469
  opacity: 0 !important;
447
470
  }
448
471
  .close-btn {
449
- width: 16px;
450
- height: 16px;
472
+ width: auto;
473
+ height: auto;
451
474
  cursor: pointer;
452
475
  margin-left: 12px;
453
476
  display: flex;
@@ -455,6 +478,18 @@
455
478
  justify-content: center;
456
479
  }
457
480
 
481
+ .df-icon-container {
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ width: 36px;
486
+ height: 36px;
487
+ border-radius: 8px;
488
+ background-color: ${config.background || "#FFFFFF"}E6;
489
+ backdrop-filter: blur(4px);
490
+ -webkit-backdrop-filter: blur(4px);
491
+ }
492
+
458
493
  /* Animation classes for FLIP */
459
494
  .df-animating {
460
495
  transition: transform .45s cubic-bezier(.4, 0, .2, 1);
@@ -474,16 +509,12 @@
474
509
  .df-btn:not(.df-closed) {
475
510
  margin: 0px;
476
511
  border-radius: 0px;
477
- padding-bottom: env(safe-area-inset-bottom, 0);
478
512
  }
479
513
 
480
514
  .df-btn:not(.df-closed) > .df-btn-content {
481
515
  width: 100vw;
482
- max-height: 100vh;
483
516
  max-height: 100dvh;
484
- height: calc(100vh - 56px);
485
- height: calc(100dvh - 56px - env(safe-area-inset-bottom, 0px));
486
- padding-bottom: 0px
517
+ height: 100dvh;
487
518
  }
488
519
 
489
520
  .df-btn-text {
@@ -491,14 +522,26 @@
491
522
  height: 56px;
492
523
  font-size: 16px;
493
524
  }
525
+
526
+ /* Hide the pseudo-element on mobile when open (we use .header-logo instead) */
527
+ .df-btn:not(.df-closed) > .df-btn-header > .df-btn-text:before {
528
+ display: none;
529
+ }
494
530
  }
495
531
 
496
- ${config.hasDarkMode ? `
532
+ ${
533
+ config.hasDarkMode
534
+ ? `
497
535
  @media (prefers-color-scheme: dark){
498
536
  .df-btn {
499
537
  background-color: ${config.backgroundDark || "#171717"}
500
538
  }
501
539
 
540
+ /* iOS Safari keyboard fix: Explicit background for dark mode */
541
+ .df-btn-content {
542
+ background-color: ${config.backgroundDark || "#171717"}
543
+ }
544
+
502
545
  .df-btn-text {
503
546
  color: ${config.fontColorDark || "#FFFFFF"}
504
547
  }
@@ -511,10 +554,6 @@
511
554
  }')
512
555
  }
513
556
 
514
- .df-btn:not(.df-closed) > .df-btn-text:before {
515
- background-image: url('${origin}/assets/close_dark.svg')
516
- }
517
-
518
557
  .df-notification {
519
558
  background: ${config.backgroundDark || "#171717"};
520
559
  color: ${config.fontColorDark || "#FFFFFF"};
@@ -523,8 +562,14 @@
523
562
  .df-notification:after {
524
563
  border-top: 6px solid ${config.backgroundDark || "#171717"};
525
564
  }
565
+
566
+ .df-icon-container {
567
+ background-color: ${config.backgroundDark || "#171717"}E6;
568
+ }
569
+ }
570
+ `
571
+ : ""
526
572
  }
527
- ` : ''}
528
573
 
529
574
  @keyframes shake {
530
575
  0%, 100% { transform: translateX(0); }
@@ -546,6 +591,57 @@
546
591
  display: none;
547
592
  }
548
593
 
594
+ /* iOS Safari keyboard background bleed fix: Override vh with dvh */
595
+ @supports (-webkit-touch-callout: none) {
596
+ .df-btn-content {
597
+ max-height: 100dvh !important;
598
+ }
599
+
600
+ .df-btn.df-maximized {
601
+ height: 100dvh !important;
602
+ }
603
+
604
+ .df-btn.df-maximized .df-btn-content {
605
+ height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
606
+ max-height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
607
+ }
608
+
609
+ @media screen and (max-width: 720px) {
610
+ .df-btn:not(.df-closed) > .df-btn-content {
611
+ max-height: 100dvh !important;
612
+ height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
613
+ }
614
+ }
615
+ }
616
+
617
+ /* iOS Safari: Force solid background painting to prevent transparency during keyboard animation */
618
+ @supports (-webkit-touch-callout: none) {
619
+ .df-btn {
620
+ background-color: ${config.background || "#FEFFFF"} !important;
621
+ -webkit-backface-visibility: hidden;
622
+ backface-visibility: hidden;
623
+ }
624
+
625
+ .df-btn-content {
626
+ background-color: ${config.background || "#FEFFFF"} !important;
627
+ }
628
+
629
+ ${
630
+ config.hasDarkMode
631
+ ? `
632
+ @media (prefers-color-scheme: dark) {
633
+ .df-btn {
634
+ background-color: ${config.backgroundDark || "#171717"} !important;
635
+ }
636
+ .df-btn-content {
637
+ background-color: ${config.backgroundDark || "#171717"} !important;
638
+ }
639
+ }
640
+ `
641
+ : ""
642
+ }
643
+ }
644
+
549
645
  .df-btn-loading .df-btn-text:after {
550
646
  content: '';
551
647
  display: inline-block;
@@ -647,7 +743,7 @@
647
743
  } catch (e) {
648
744
  console.warn(
649
745
  "[OSW Widget] Could not save notification dismissed state:",
650
- e
746
+ e,
651
747
  );
652
748
  }
653
749
  }
@@ -700,6 +796,56 @@
700
796
  }
701
797
  };
702
798
 
799
+ // iOS Safari detection for keyboard background bleed fix
800
+ const isMobileDevice =
801
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
802
+ navigator.userAgent,
803
+ );
804
+
805
+ // Store original body styles for scroll lock restoration
806
+ let savedBodyStyles = null;
807
+
808
+ // Lock body scroll on mobile to prevent background bleed when keyboard opens (iOS Safari fix)
809
+ function lockBodyScroll() {
810
+ if (!isMobileDevice) return;
811
+
812
+ savedBodyStyles = {
813
+ position: document.body.style.position,
814
+ top: document.body.style.top,
815
+ left: document.body.style.left,
816
+ right: document.body.style.right,
817
+ width: document.body.style.width,
818
+ height: document.body.style.height,
819
+ overflow: document.body.style.overflow,
820
+ scrollY: window.scrollY,
821
+ };
822
+
823
+ // Lock the body to prevent background scroll during keyboard open
824
+ document.body.style.position = "fixed";
825
+ document.body.style.top = `-${savedBodyStyles.scrollY}px`;
826
+ document.body.style.left = "0";
827
+ document.body.style.right = "0";
828
+ document.body.style.width = "100%";
829
+ document.body.style.overflow = "hidden";
830
+ }
831
+
832
+ // Restore body scroll when chat closes
833
+ function unlockBodyScroll() {
834
+ if (!isMobileDevice || !savedBodyStyles) return;
835
+
836
+ document.body.style.position = savedBodyStyles.position;
837
+ document.body.style.top = savedBodyStyles.top;
838
+ document.body.style.left = savedBodyStyles.left;
839
+ document.body.style.right = savedBodyStyles.right;
840
+ document.body.style.width = savedBodyStyles.width;
841
+ document.body.style.height = savedBodyStyles.height;
842
+ document.body.style.overflow = savedBodyStyles.overflow;
843
+
844
+ // Restore scroll position
845
+ window.scrollTo(0, savedBodyStyles.scrollY);
846
+ savedBodyStyles = null;
847
+ }
848
+
703
849
  // Create and inject the iframe on first open
704
850
  function ensureIframeLoaded() {
705
851
  return new Promise((resolve) => {
@@ -723,6 +869,29 @@
723
869
  iframe.allow = "microphone *";
724
870
  iframe.style.opacity = "0";
725
871
 
872
+ // iOS Safari keyboard background bleed fix: Make iframe a fixed-position repaint boundary on mobile
873
+ if (isMobileDevice) {
874
+ iframe.style.position = "fixed";
875
+ iframe.style.top = "0";
876
+ iframe.style.bottom = "0"; // Stretch from top to bottom
877
+ iframe.style.left = "0";
878
+ iframe.style.right = "0";
879
+ iframe.style.width = "100vw";
880
+ iframe.style.height = "100%"; // Fill between top and bottom
881
+ iframe.style.zIndex = "10000"; // Higher than widget container (999)
882
+ // Explicit background color to prevent transparency during iOS keyboard animation
883
+ const systemDarkMode =
884
+ config.hasDarkMode &&
885
+ window.matchMedia &&
886
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
887
+ iframe.style.backgroundColor = systemDarkMode
888
+ ? config.backgroundDark || "#171717"
889
+ : config.background || "#FEFFFF";
890
+ // Force GPU layer for better repaint boundary
891
+ iframe.style.transform = "translateZ(0)";
892
+ iframe.style.webkitTransform = "translateZ(0)";
893
+ }
894
+
726
895
  iframe.addEventListener("load", () => {
727
896
  iframe.style.opacity = "";
728
897
  iframeLoaded = true;
@@ -749,6 +918,8 @@
749
918
  if (oldClose) header.removeChild(oldClose);
750
919
  const oldMax = header.querySelector(".maximize-minimize-btn");
751
920
  if (oldMax) header.removeChild(oldMax);
921
+ const oldLogo = header.querySelector(".header-logo");
922
+ if (oldLogo) header.removeChild(oldLogo);
752
923
  const btnText = header.querySelector(".df-btn-text");
753
924
  const btn = document.querySelector(".df-btn");
754
925
  if (!btn) return;
@@ -759,38 +930,62 @@
759
930
  // remove right padding from df-btn-text
760
931
  btnText.style.paddingRight = "0";
761
932
 
762
- // Only add maximize/minimize button on desktop
763
-
764
- // Add maximize/minimize button (right)
765
- const maxBtn = document.createElement("div");
766
- maxBtn.className = "maximize-minimize-btn";
767
- const maximized = btn.classList.contains("df-maximized");
768
- const isDarkMode =
933
+ // Detect mobile view for conditional button display
934
+ const isMobileView = window.innerWidth <= 720;
935
+ const systemDarkMode =
769
936
  config.hasDarkMode &&
770
937
  window.matchMedia &&
771
938
  window.matchMedia("(prefers-color-scheme: dark)").matches;
772
- maxBtn.innerHTML = `<img class="maximize-minimize-icon" src="${origin}/assets/${
773
- maximized ? "collapse" : "expand"
774
- }${isDarkMode ? "_dark" : ""}.svg" alt="${
775
- maximized ? "Minimize" : "Maximize"
776
- }" style="width:24px;height:24px;cursor:pointer;" />`;
777
- header.appendChild(maxBtn);
778
- maxBtn.addEventListener("click", (e) => {
779
- e.stopPropagation();
780
- maximizeMinimize();
781
- });
782
-
783
- // Add close button (left)
784
- const closeBtn = document.createElement("div");
785
- closeBtn.className = "close-btn";
786
- closeBtn.innerHTML = `<img src="${origin}/assets/close${
787
- isDarkMode ? "_dark" : ""
788
- }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" />`;
789
- header.insertBefore(closeBtn, btnText);
790
- closeBtn.addEventListener("click", (e) => {
791
- e.stopPropagation();
792
- dfToggle();
793
- });
939
+ // Use white icons only if the actual background is dark
940
+ const currentBg = systemDarkMode
941
+ ? config.backgroundDark
942
+ : config.background;
943
+ const useDarkIcons = isColorDark(currentBg);
944
+
945
+ if (!isMobileView) {
946
+ // Desktop layout: [Expand] ... [Close]
947
+
948
+ // Expand button on LEFT
949
+ const maxBtn = document.createElement("div");
950
+ maxBtn.className = "maximize-minimize-btn";
951
+ const maximized = btn.classList.contains("df-maximized");
952
+ maxBtn.innerHTML = `<div class="df-icon-container"><img class="maximize-minimize-icon" src="${origin}/assets/${
953
+ maximized ? "collapse" : "expand"
954
+ }${useDarkIcons ? "_dark" : ""}.svg" alt="${
955
+ maximized ? "Minimize" : "Maximize"
956
+ }" style="width:24px;height:24px;cursor:pointer;" /></div>`;
957
+ header.insertBefore(maxBtn, btnText); // LEFT
958
+ maxBtn.addEventListener("click", (e) => {
959
+ e.stopPropagation();
960
+ maximizeMinimize();
961
+ });
962
+
963
+ // Close button on RIGHT
964
+ const closeBtn = document.createElement("div");
965
+ closeBtn.className = "close-btn";
966
+ closeBtn.innerHTML = `<div class="df-icon-container"><img src="${origin}/assets/close${
967
+ useDarkIcons ? "_dark" : ""
968
+ }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" /></div>`;
969
+ header.appendChild(closeBtn); // RIGHT
970
+ closeBtn.addEventListener("click", (e) => {
971
+ e.stopPropagation();
972
+ dfToggle();
973
+ });
974
+ } else {
975
+ // Mobile layout: [Close] on right only
976
+
977
+ // Close button on RIGHT
978
+ const closeBtn = document.createElement("div");
979
+ closeBtn.className = "close-btn";
980
+ closeBtn.innerHTML = `<div class="df-icon-container"><img src="${origin}/assets/close${
981
+ useDarkIcons ? "_dark" : ""
982
+ }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" /></div>`;
983
+ header.appendChild(closeBtn); // RIGHT
984
+ closeBtn.addEventListener("click", (e) => {
985
+ e.stopPropagation();
986
+ dfToggle();
987
+ });
988
+ }
794
989
  } else {
795
990
  // Set text to openText
796
991
  if (btnText) btnText.innerText = config.openText || "Chat";
@@ -861,6 +1056,13 @@
861
1056
  // Always remove maximized state when closing
862
1057
  if (btn.classList.contains("df-closed")) {
863
1058
  btn.classList.remove("df-maximized");
1059
+ // iOS Safari keyboard fix: Unlock body scroll when chat closes
1060
+ unlockBodyScroll();
1061
+ } else {
1062
+ // iOS Safari keyboard fix: Lock body scroll when chat opens on mobile
1063
+ if (isMobileDevice) {
1064
+ lockBodyScroll();
1065
+ }
864
1066
  }
865
1067
  updateHeaderButtons();
866
1068
  clearInactivityAndShake();
@@ -900,7 +1102,7 @@
900
1102
  } catch (e) {
901
1103
  console.warn(
902
1104
  "[OSW Widget] Could not save notification dismissed state:",
903
- e
1105
+ e,
904
1106
  );
905
1107
  }
906
1108
 
@@ -912,6 +1114,9 @@
912
1114
  const isMobile = window.innerWidth <= 768;
913
1115
 
914
1116
  if (isMobile) {
1117
+ // iOS Safari keyboard fix: Lock body scroll when opening chat on mobile
1118
+ lockBodyScroll();
1119
+
915
1120
  // On mobile, apply FLIP animation for open+maximize
916
1121
  const content = btn.querySelector(".df-btn-content");
917
1122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aumera-on-screen-widget",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "A lightweight, customizable chat widget for websites",
5
5
  "main": "df-btn.js",
6
6
  "scripts": {