aumera-on-screen-widget 0.0.20 → 0.0.22

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 +259 -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) {
@@ -150,7 +150,7 @@
150
150
  showNotification: false,
151
151
  };
152
152
 
153
- // Track if dark mode was explicitly requested via HTML attribute
153
+ // Track if dark mode was explicitly requested
154
154
  const hasExplicitDarkBackground = !!wrapper.getAttribute("backgroundDark");
155
155
 
156
156
  // Initial config from HTML attributes with fallback to defaults
@@ -200,10 +200,10 @@
200
200
  // Fetch settings from GraphQL and merge with config
201
201
  if (!config.orgId) {
202
202
  console.error(
203
- "[OSW Widget] No organization ID provided! Widget will not be displayed."
203
+ "[OSW Widget] No organization ID provided! Widget will not be displayed.",
204
204
  );
205
205
  console.error(
206
- "[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' ...>",
207
207
  );
208
208
  return; // Exit if no orgId
209
209
  }
@@ -211,12 +211,12 @@
211
211
  const settings = await fetchChatbotSettings(
212
212
  config.orgId,
213
213
  config.env,
214
- detectedLang
214
+ detectedLang,
215
215
  );
216
216
 
217
217
  if (!settings) {
218
218
  console.warn(
219
- "[OSW Widget] Failed to fetch chatbot settings. Using fallback configuration."
219
+ "[OSW Widget] Failed to fetch chatbot settings. Using fallback configuration.",
220
220
  );
221
221
  console.warn("[OSW Widget] Check:");
222
222
  console.warn("[OSW Widget] 1. Network tab for CORS errors");
@@ -235,7 +235,8 @@
235
235
  config.backgroundDark =
236
236
  settings.oswColourBackgroundDark || config.backgroundDark;
237
237
  // Update hasDarkMode if GraphQL provides dark background
238
- config.hasDarkMode = config.hasDarkMode || !!settings.oswColourBackgroundDark;
238
+ config.hasDarkMode =
239
+ config.hasDarkMode || !!settings.oswColourBackgroundDark;
239
240
  config.fontColor = settings.oswColourFontLight || config.fontColor;
240
241
  config.fontColorDark = settings.oswColourFontDark || config.fontColorDark;
241
242
  config.showNotification =
@@ -248,7 +249,7 @@
248
249
  config.logoDark = cloudinaryResize(settings.chatbotMask, 64, 64);
249
250
  console.log(
250
251
  "[OSW Widget] Using chatbotMask as logo:",
251
- settings.chatbotMask
252
+ settings.chatbotMask,
252
253
  );
253
254
  }
254
255
  }
@@ -361,6 +362,18 @@
361
362
  height: 48px;
362
363
  }
363
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
+
364
377
  .df-btn-text:before {
365
378
  background-position: center;
366
379
  background-repeat: no-repeat;
@@ -382,11 +395,12 @@
382
395
  }
383
396
 
384
397
  .df-btn:not(.df-closed){
385
- border-radius: 16px
398
+ border-radius: 16px;
399
+ overflow: hidden;
386
400
  }
387
401
 
388
- .df-btn:not(.df-closed) > .df-btn-text:before {
389
- background-image: url('${origin}/assets/close.svg')
402
+ .df-btn:not(.df-closed) .df-btn-text:before {
403
+ display: none;
390
404
  }
391
405
 
392
406
  .df-btn-content {
@@ -403,6 +417,8 @@
403
417
  ${config.position === "left" ? "float: left;" : "float: right;"}
404
418
  opacity: 1;
405
419
  border-radius: 0 0 16px 16px;
420
+ /* iOS Safari keyboard fix: Explicit background to prevent transparency bleed */
421
+ background-color: ${config.background || "#FEFFFF"};
406
422
  }
407
423
 
408
424
  .df-btn:not(.df-closed) > .df-btn-content {
@@ -417,8 +433,8 @@
417
433
  }
418
434
 
419
435
  .maximize-minimize-btn {
420
- width: 16px;
421
- height: 16px;
436
+ width: auto;
437
+ height: auto;
422
438
  cursor: pointer;
423
439
  margin-right: 12px;
424
440
  display: flex;
@@ -436,16 +452,13 @@
436
452
  bottom: 0 !important;
437
453
  margin: 0 !important;
438
454
  border-radius: 0 !important;
439
- /* Handle safe areas on mobile devices */
440
- padding-bottom: env(safe-area-inset-bottom, 0);
455
+ z-index: 10000;
441
456
  }
442
457
  .df-btn.df-maximized .df-btn-content {
443
458
  width: 100vw !important;
444
459
  max-width: 100vw !important;
445
- height: calc(100vh - 56px) !important;
446
- height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important; /* Account for safe area */
447
- max-height: calc(100vh - 56px) !important;
448
- max-height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
460
+ height: 100dvh !important;
461
+ max-height: 100dvh !important;
449
462
  opacity: 1 !important;
450
463
  float: none !important;
451
464
  display: block !important;
@@ -457,8 +470,8 @@
457
470
  opacity: 0 !important;
458
471
  }
459
472
  .close-btn {
460
- width: 16px;
461
- height: 16px;
473
+ width: auto;
474
+ height: auto;
462
475
  cursor: pointer;
463
476
  margin-left: 12px;
464
477
  display: flex;
@@ -466,6 +479,18 @@
466
479
  justify-content: center;
467
480
  }
468
481
 
482
+ .df-icon-container {
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ width: 36px;
487
+ height: 36px;
488
+ border-radius: 8px;
489
+ background-color: ${config.background || "#FFFFFF"}E6;
490
+ backdrop-filter: blur(4px);
491
+ -webkit-backdrop-filter: blur(4px);
492
+ }
493
+
469
494
  /* Animation classes for FLIP */
470
495
  .df-animating {
471
496
  transition: transform .45s cubic-bezier(.4, 0, .2, 1);
@@ -485,16 +510,12 @@
485
510
  .df-btn:not(.df-closed) {
486
511
  margin: 0px;
487
512
  border-radius: 0px;
488
- padding-bottom: env(safe-area-inset-bottom, 0);
489
513
  }
490
514
 
491
515
  .df-btn:not(.df-closed) > .df-btn-content {
492
516
  width: 100vw;
493
- max-height: 100vh;
494
517
  max-height: 100dvh;
495
- height: calc(100vh - 56px);
496
- height: calc(100dvh - 56px - env(safe-area-inset-bottom, 0px));
497
- padding-bottom: 0px
518
+ height: 100dvh;
498
519
  }
499
520
 
500
521
  .df-btn-text {
@@ -502,14 +523,26 @@
502
523
  height: 56px;
503
524
  font-size: 16px;
504
525
  }
526
+
527
+ /* Hide the pseudo-element on mobile when open (we use .header-logo instead) */
528
+ .df-btn:not(.df-closed) > .df-btn-header > .df-btn-text:before {
529
+ display: none;
530
+ }
505
531
  }
506
532
 
507
- ${config.hasDarkMode ? `
533
+ ${
534
+ config.hasDarkMode
535
+ ? `
508
536
  @media (prefers-color-scheme: dark){
509
537
  .df-btn {
510
538
  background-color: ${config.backgroundDark || "#171717"}
511
539
  }
512
540
 
541
+ /* iOS Safari keyboard fix: Explicit background for dark mode */
542
+ .df-btn-content {
543
+ background-color: ${config.backgroundDark || "#171717"}
544
+ }
545
+
513
546
  .df-btn-text {
514
547
  color: ${config.fontColorDark || "#FFFFFF"}
515
548
  }
@@ -522,10 +555,6 @@
522
555
  }')
523
556
  }
524
557
 
525
- .df-btn:not(.df-closed) > .df-btn-text:before {
526
- background-image: url('${origin}/assets/close${isColorDark(config.backgroundDark) ? "_dark" : ""}.svg')
527
- }
528
-
529
558
  .df-notification {
530
559
  background: ${config.backgroundDark || "#171717"};
531
560
  color: ${config.fontColorDark || "#FFFFFF"};
@@ -534,8 +563,14 @@
534
563
  .df-notification:after {
535
564
  border-top: 6px solid ${config.backgroundDark || "#171717"};
536
565
  }
566
+
567
+ .df-icon-container {
568
+ background-color: ${config.backgroundDark || "#171717"}E6;
569
+ }
570
+ }
571
+ `
572
+ : ""
537
573
  }
538
- ` : ''}
539
574
 
540
575
  @keyframes shake {
541
576
  0%, 100% { transform: translateX(0); }
@@ -557,6 +592,57 @@
557
592
  display: none;
558
593
  }
559
594
 
595
+ /* iOS Safari keyboard background bleed fix: Override vh with dvh */
596
+ @supports (-webkit-touch-callout: none) {
597
+ .df-btn-content {
598
+ max-height: 100dvh !important;
599
+ }
600
+
601
+ .df-btn.df-maximized {
602
+ height: 100dvh !important;
603
+ }
604
+
605
+ .df-btn.df-maximized .df-btn-content {
606
+ height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
607
+ max-height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
608
+ }
609
+
610
+ @media screen and (max-width: 720px) {
611
+ .df-btn:not(.df-closed) > .df-btn-content {
612
+ max-height: 100dvh !important;
613
+ height: calc(100dvh - env(safe-area-inset-bottom, 0px)) !important;
614
+ }
615
+ }
616
+ }
617
+
618
+ /* iOS Safari: Force solid background painting to prevent transparency during keyboard animation */
619
+ @supports (-webkit-touch-callout: none) {
620
+ .df-btn {
621
+ background-color: ${config.background || "#FEFFFF"} !important;
622
+ -webkit-backface-visibility: hidden;
623
+ backface-visibility: hidden;
624
+ }
625
+
626
+ .df-btn-content {
627
+ background-color: ${config.background || "#FEFFFF"} !important;
628
+ }
629
+
630
+ ${
631
+ config.hasDarkMode
632
+ ? `
633
+ @media (prefers-color-scheme: dark) {
634
+ .df-btn {
635
+ background-color: ${config.backgroundDark || "#171717"} !important;
636
+ }
637
+ .df-btn-content {
638
+ background-color: ${config.backgroundDark || "#171717"} !important;
639
+ }
640
+ }
641
+ `
642
+ : ""
643
+ }
644
+ }
645
+
560
646
  .df-btn-loading .df-btn-text:after {
561
647
  content: '';
562
648
  display: inline-block;
@@ -658,7 +744,7 @@
658
744
  } catch (e) {
659
745
  console.warn(
660
746
  "[OSW Widget] Could not save notification dismissed state:",
661
- e
747
+ e,
662
748
  );
663
749
  }
664
750
  }
@@ -711,6 +797,56 @@
711
797
  }
712
798
  };
713
799
 
800
+ // iOS Safari detection for keyboard background bleed fix
801
+ const isMobileDevice =
802
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
803
+ navigator.userAgent,
804
+ );
805
+
806
+ // Store original body styles for scroll lock restoration
807
+ let savedBodyStyles = null;
808
+
809
+ // Lock body scroll on mobile to prevent background bleed when keyboard opens (iOS Safari fix)
810
+ function lockBodyScroll() {
811
+ if (!isMobileDevice) return;
812
+
813
+ savedBodyStyles = {
814
+ position: document.body.style.position,
815
+ top: document.body.style.top,
816
+ left: document.body.style.left,
817
+ right: document.body.style.right,
818
+ width: document.body.style.width,
819
+ height: document.body.style.height,
820
+ overflow: document.body.style.overflow,
821
+ scrollY: window.scrollY,
822
+ };
823
+
824
+ // Lock the body to prevent background scroll during keyboard open
825
+ document.body.style.position = "fixed";
826
+ document.body.style.top = `-${savedBodyStyles.scrollY}px`;
827
+ document.body.style.left = "0";
828
+ document.body.style.right = "0";
829
+ document.body.style.width = "100%";
830
+ document.body.style.overflow = "hidden";
831
+ }
832
+
833
+ // Restore body scroll when chat closes
834
+ function unlockBodyScroll() {
835
+ if (!isMobileDevice || !savedBodyStyles) return;
836
+
837
+ document.body.style.position = savedBodyStyles.position;
838
+ document.body.style.top = savedBodyStyles.top;
839
+ document.body.style.left = savedBodyStyles.left;
840
+ document.body.style.right = savedBodyStyles.right;
841
+ document.body.style.width = savedBodyStyles.width;
842
+ document.body.style.height = savedBodyStyles.height;
843
+ document.body.style.overflow = savedBodyStyles.overflow;
844
+
845
+ // Restore scroll position
846
+ window.scrollTo(0, savedBodyStyles.scrollY);
847
+ savedBodyStyles = null;
848
+ }
849
+
714
850
  // Create and inject the iframe on first open
715
851
  function ensureIframeLoaded() {
716
852
  return new Promise((resolve) => {
@@ -734,6 +870,29 @@
734
870
  iframe.allow = "microphone *";
735
871
  iframe.style.opacity = "0";
736
872
 
873
+ // iOS Safari keyboard background bleed fix: Make iframe a fixed-position repaint boundary on mobile
874
+ if (isMobileDevice) {
875
+ iframe.style.position = "fixed";
876
+ iframe.style.top = "0";
877
+ iframe.style.bottom = "0"; // Stretch from top to bottom
878
+ iframe.style.left = "0";
879
+ iframe.style.right = "0";
880
+ iframe.style.width = "100vw";
881
+ iframe.style.height = "100%"; // Fill between top and bottom
882
+ iframe.style.zIndex = "10000"; // Higher than widget container (999)
883
+ // Explicit background color to prevent transparency during iOS keyboard animation
884
+ const systemDarkMode =
885
+ config.hasDarkMode &&
886
+ window.matchMedia &&
887
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
888
+ iframe.style.backgroundColor = systemDarkMode
889
+ ? config.backgroundDark || "#171717"
890
+ : config.background || "#FEFFFF";
891
+ // Force GPU layer for better repaint boundary
892
+ iframe.style.transform = "translateZ(0)";
893
+ iframe.style.webkitTransform = "translateZ(0)";
894
+ }
895
+
737
896
  iframe.addEventListener("load", () => {
738
897
  iframe.style.opacity = "";
739
898
  iframeLoaded = true;
@@ -760,6 +919,8 @@
760
919
  if (oldClose) header.removeChild(oldClose);
761
920
  const oldMax = header.querySelector(".maximize-minimize-btn");
762
921
  if (oldMax) header.removeChild(oldMax);
922
+ const oldLogo = header.querySelector(".header-logo");
923
+ if (oldLogo) header.removeChild(oldLogo);
763
924
  const btnText = header.querySelector(".df-btn-text");
764
925
  const btn = document.querySelector(".df-btn");
765
926
  if (!btn) return;
@@ -770,41 +931,62 @@
770
931
  // remove right padding from df-btn-text
771
932
  btnText.style.paddingRight = "0";
772
933
 
773
- // Only add maximize/minimize button on desktop
774
-
775
- // Add maximize/minimize button (right)
776
- const maxBtn = document.createElement("div");
777
- maxBtn.className = "maximize-minimize-btn";
778
- const maximized = btn.classList.contains("df-maximized");
934
+ // Detect mobile view for conditional button display
935
+ const isMobileView = window.innerWidth <= 720;
779
936
  const systemDarkMode =
780
937
  config.hasDarkMode &&
781
938
  window.matchMedia &&
782
939
  window.matchMedia("(prefers-color-scheme: dark)").matches;
783
940
  // Use white icons only if the actual background is dark
784
- const currentBg = systemDarkMode ? config.backgroundDark : config.background;
941
+ const currentBg = systemDarkMode
942
+ ? config.backgroundDark
943
+ : config.background;
785
944
  const useDarkIcons = isColorDark(currentBg);
786
- maxBtn.innerHTML = `<img class="maximize-minimize-icon" src="${origin}/assets/${
787
- maximized ? "collapse" : "expand"
788
- }${useDarkIcons ? "_dark" : ""}.svg" alt="${
789
- maximized ? "Minimize" : "Maximize"
790
- }" style="width:24px;height:24px;cursor:pointer;" />`;
791
- header.appendChild(maxBtn);
792
- maxBtn.addEventListener("click", (e) => {
793
- e.stopPropagation();
794
- maximizeMinimize();
795
- });
796
945
 
797
- // Add close button (left)
798
- const closeBtn = document.createElement("div");
799
- closeBtn.className = "close-btn";
800
- closeBtn.innerHTML = `<img src="${origin}/assets/close${
801
- useDarkIcons ? "_dark" : ""
802
- }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" />`;
803
- header.insertBefore(closeBtn, btnText);
804
- closeBtn.addEventListener("click", (e) => {
805
- e.stopPropagation();
806
- dfToggle();
807
- });
946
+ if (!isMobileView) {
947
+ // Desktop layout: [Expand] ... [Close]
948
+
949
+ // Expand button on LEFT
950
+ const maxBtn = document.createElement("div");
951
+ maxBtn.className = "maximize-minimize-btn";
952
+ const maximized = btn.classList.contains("df-maximized");
953
+ maxBtn.innerHTML = `<div class="df-icon-container"><img class="maximize-minimize-icon" src="${origin}/assets/${
954
+ maximized ? "collapse" : "expand"
955
+ }${useDarkIcons ? "_dark" : ""}.svg" alt="${
956
+ maximized ? "Minimize" : "Maximize"
957
+ }" style="width:24px;height:24px;cursor:pointer;" /></div>`;
958
+ header.insertBefore(maxBtn, btnText); // LEFT
959
+ maxBtn.addEventListener("click", (e) => {
960
+ e.stopPropagation();
961
+ maximizeMinimize();
962
+ });
963
+
964
+ // Close button on RIGHT
965
+ const closeBtn = document.createElement("div");
966
+ closeBtn.className = "close-btn";
967
+ closeBtn.innerHTML = `<div class="df-icon-container"><img src="${origin}/assets/close${
968
+ useDarkIcons ? "_dark" : ""
969
+ }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" /></div>`;
970
+ header.appendChild(closeBtn); // RIGHT
971
+ closeBtn.addEventListener("click", (e) => {
972
+ e.stopPropagation();
973
+ dfToggle();
974
+ });
975
+ } else {
976
+ // Mobile layout: [Close] on right only
977
+
978
+ // Close button on RIGHT
979
+ const closeBtn = document.createElement("div");
980
+ closeBtn.className = "close-btn";
981
+ closeBtn.innerHTML = `<div class="df-icon-container"><img src="${origin}/assets/close${
982
+ useDarkIcons ? "_dark" : ""
983
+ }.svg" alt="Close" style="width:20px;height:20px;cursor:pointer;" /></div>`;
984
+ header.appendChild(closeBtn); // RIGHT
985
+ closeBtn.addEventListener("click", (e) => {
986
+ e.stopPropagation();
987
+ dfToggle();
988
+ });
989
+ }
808
990
  } else {
809
991
  // Set text to openText
810
992
  if (btnText) btnText.innerText = config.openText || "Chat";
@@ -875,6 +1057,13 @@
875
1057
  // Always remove maximized state when closing
876
1058
  if (btn.classList.contains("df-closed")) {
877
1059
  btn.classList.remove("df-maximized");
1060
+ // iOS Safari keyboard fix: Unlock body scroll when chat closes
1061
+ unlockBodyScroll();
1062
+ } else {
1063
+ // iOS Safari keyboard fix: Lock body scroll when chat opens on mobile
1064
+ if (isMobileDevice) {
1065
+ lockBodyScroll();
1066
+ }
878
1067
  }
879
1068
  updateHeaderButtons();
880
1069
  clearInactivityAndShake();
@@ -914,7 +1103,7 @@
914
1103
  } catch (e) {
915
1104
  console.warn(
916
1105
  "[OSW Widget] Could not save notification dismissed state:",
917
- e
1106
+ e,
918
1107
  );
919
1108
  }
920
1109
 
@@ -926,6 +1115,9 @@
926
1115
  const isMobile = window.innerWidth <= 768;
927
1116
 
928
1117
  if (isMobile) {
1118
+ // iOS Safari keyboard fix: Lock body scroll when opening chat on mobile
1119
+ lockBodyScroll();
1120
+
929
1121
  // On mobile, apply FLIP animation for open+maximize
930
1122
  const content = btn.querySelector(".df-btn-content");
931
1123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aumera-on-screen-widget",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "A lightweight, customizable chat widget for websites",
5
5
  "main": "df-btn.js",
6
6
  "scripts": {