aumera-on-screen-widget 0.0.20 → 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 +257 -66
  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) {
@@ -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,12 @@
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);
441
455
  }
442
456
  .df-btn.df-maximized .df-btn-content {
443
457
  width: 100vw !important;
444
458
  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;
459
+ height: 100dvh !important;
460
+ max-height: 100dvh !important;
449
461
  opacity: 1 !important;
450
462
  float: none !important;
451
463
  display: block !important;
@@ -457,8 +469,8 @@
457
469
  opacity: 0 !important;
458
470
  }
459
471
  .close-btn {
460
- width: 16px;
461
- height: 16px;
472
+ width: auto;
473
+ height: auto;
462
474
  cursor: pointer;
463
475
  margin-left: 12px;
464
476
  display: flex;
@@ -466,6 +478,18 @@
466
478
  justify-content: center;
467
479
  }
468
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
+
469
493
  /* Animation classes for FLIP */
470
494
  .df-animating {
471
495
  transition: transform .45s cubic-bezier(.4, 0, .2, 1);
@@ -485,16 +509,12 @@
485
509
  .df-btn:not(.df-closed) {
486
510
  margin: 0px;
487
511
  border-radius: 0px;
488
- padding-bottom: env(safe-area-inset-bottom, 0);
489
512
  }
490
513
 
491
514
  .df-btn:not(.df-closed) > .df-btn-content {
492
515
  width: 100vw;
493
- max-height: 100vh;
494
516
  max-height: 100dvh;
495
- height: calc(100vh - 56px);
496
- height: calc(100dvh - 56px - env(safe-area-inset-bottom, 0px));
497
- padding-bottom: 0px
517
+ height: 100dvh;
498
518
  }
499
519
 
500
520
  .df-btn-text {
@@ -502,14 +522,26 @@
502
522
  height: 56px;
503
523
  font-size: 16px;
504
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
+ }
505
530
  }
506
531
 
507
- ${config.hasDarkMode ? `
532
+ ${
533
+ config.hasDarkMode
534
+ ? `
508
535
  @media (prefers-color-scheme: dark){
509
536
  .df-btn {
510
537
  background-color: ${config.backgroundDark || "#171717"}
511
538
  }
512
539
 
540
+ /* iOS Safari keyboard fix: Explicit background for dark mode */
541
+ .df-btn-content {
542
+ background-color: ${config.backgroundDark || "#171717"}
543
+ }
544
+
513
545
  .df-btn-text {
514
546
  color: ${config.fontColorDark || "#FFFFFF"}
515
547
  }
@@ -522,10 +554,6 @@
522
554
  }')
523
555
  }
524
556
 
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
557
  .df-notification {
530
558
  background: ${config.backgroundDark || "#171717"};
531
559
  color: ${config.fontColorDark || "#FFFFFF"};
@@ -534,8 +562,14 @@
534
562
  .df-notification:after {
535
563
  border-top: 6px solid ${config.backgroundDark || "#171717"};
536
564
  }
565
+
566
+ .df-icon-container {
567
+ background-color: ${config.backgroundDark || "#171717"}E6;
568
+ }
569
+ }
570
+ `
571
+ : ""
537
572
  }
538
- ` : ''}
539
573
 
540
574
  @keyframes shake {
541
575
  0%, 100% { transform: translateX(0); }
@@ -557,6 +591,57 @@
557
591
  display: none;
558
592
  }
559
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
+
560
645
  .df-btn-loading .df-btn-text:after {
561
646
  content: '';
562
647
  display: inline-block;
@@ -658,7 +743,7 @@
658
743
  } catch (e) {
659
744
  console.warn(
660
745
  "[OSW Widget] Could not save notification dismissed state:",
661
- e
746
+ e,
662
747
  );
663
748
  }
664
749
  }
@@ -711,6 +796,56 @@
711
796
  }
712
797
  };
713
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
+
714
849
  // Create and inject the iframe on first open
715
850
  function ensureIframeLoaded() {
716
851
  return new Promise((resolve) => {
@@ -734,6 +869,29 @@
734
869
  iframe.allow = "microphone *";
735
870
  iframe.style.opacity = "0";
736
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
+
737
895
  iframe.addEventListener("load", () => {
738
896
  iframe.style.opacity = "";
739
897
  iframeLoaded = true;
@@ -760,6 +918,8 @@
760
918
  if (oldClose) header.removeChild(oldClose);
761
919
  const oldMax = header.querySelector(".maximize-minimize-btn");
762
920
  if (oldMax) header.removeChild(oldMax);
921
+ const oldLogo = header.querySelector(".header-logo");
922
+ if (oldLogo) header.removeChild(oldLogo);
763
923
  const btnText = header.querySelector(".df-btn-text");
764
924
  const btn = document.querySelector(".df-btn");
765
925
  if (!btn) return;
@@ -770,41 +930,62 @@
770
930
  // remove right padding from df-btn-text
771
931
  btnText.style.paddingRight = "0";
772
932
 
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");
933
+ // Detect mobile view for conditional button display
934
+ const isMobileView = window.innerWidth <= 720;
779
935
  const systemDarkMode =
780
936
  config.hasDarkMode &&
781
937
  window.matchMedia &&
782
938
  window.matchMedia("(prefers-color-scheme: dark)").matches;
783
939
  // Use white icons only if the actual background is dark
784
- const currentBg = systemDarkMode ? config.backgroundDark : config.background;
940
+ const currentBg = systemDarkMode
941
+ ? config.backgroundDark
942
+ : config.background;
785
943
  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
944
 
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
- });
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
+ }
808
989
  } else {
809
990
  // Set text to openText
810
991
  if (btnText) btnText.innerText = config.openText || "Chat";
@@ -875,6 +1056,13 @@
875
1056
  // Always remove maximized state when closing
876
1057
  if (btn.classList.contains("df-closed")) {
877
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
+ }
878
1066
  }
879
1067
  updateHeaderButtons();
880
1068
  clearInactivityAndShake();
@@ -914,7 +1102,7 @@
914
1102
  } catch (e) {
915
1103
  console.warn(
916
1104
  "[OSW Widget] Could not save notification dismissed state:",
917
- e
1105
+ e,
918
1106
  );
919
1107
  }
920
1108
 
@@ -926,6 +1114,9 @@
926
1114
  const isMobile = window.innerWidth <= 768;
927
1115
 
928
1116
  if (isMobile) {
1117
+ // iOS Safari keyboard fix: Lock body scroll when opening chat on mobile
1118
+ lockBodyScroll();
1119
+
929
1120
  // On mobile, apply FLIP animation for open+maximize
930
1121
  const content = btn.querySelector(".df-btn-content");
931
1122
 
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.21",
4
4
  "description": "A lightweight, customizable chat widget for websites",
5
5
  "main": "df-btn.js",
6
6
  "scripts": {