@tolinku/web-sdk 0.2.0 → 0.3.0

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.mjs CHANGED
@@ -546,9 +546,24 @@ var Deferred = class {
546
546
  timezone: options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
547
547
  language: options.language || navigator.language,
548
548
  screen_width: options.screenWidth || window.screen.width,
549
- screen_height: options.screenHeight || window.screen.height
549
+ screen_height: options.screenHeight || window.screen.height,
550
+ // Separates devices reporting identical logical dimensions.
551
+ device_pixel_ratio: options.devicePixelRatio || window.devicePixelRatio || 1
550
552
  });
551
553
  } catch (err) {
554
+ const status = err?.statusCode ?? err?.status;
555
+ if (status === 404) {
556
+ return null;
557
+ }
558
+ if (status === 403) {
559
+ console.warn(
560
+ "[Tolinku] Failed to claim deferred link by signals: HTTP 403.",
561
+ "Check that appspaceId is your Appspace ID (copy it from the dashboard",
562
+ "under Settings), not your subdomain or slug.",
563
+ err
564
+ );
565
+ return null;
566
+ }
552
567
  console.warn("[Tolinku] Failed to claim deferred link by signals:", err);
553
568
  return null;
554
569
  }
@@ -638,11 +653,56 @@ function sanitizeCssColor(value) {
638
653
  }
639
654
 
640
655
  // src/banners.ts
656
+ var themes = {
657
+ light: {
658
+ bg: "#ffffff",
659
+ border: "",
660
+ shadow: "sm",
661
+ title: { color: "#000000", size: 14, weight: 600 },
662
+ body: { color: "#000000", size: 12, weight: 400 },
663
+ cta: { size: 13, weight: 600, radius: 100 },
664
+ icon: { size: 40, radius: 10 }
665
+ },
666
+ dark: {
667
+ bg: "#1B1B1B",
668
+ border: "",
669
+ shadow: "sm",
670
+ title: { color: "#ffffff", size: 14, weight: 600 },
671
+ body: { color: "#ffffff", size: 12, weight: 400 },
672
+ cta: { size: 13, weight: 600, radius: 100 },
673
+ icon: { size: 40, radius: 10 }
674
+ }
675
+ };
676
+ var SHADOW_VALUES = {
677
+ none: "none",
678
+ sm: "0 2px 8px rgba(0,0,0,0.15)",
679
+ md: "0 6px 20px rgba(0,0,0,0.18)",
680
+ lg: "0 12px 36px rgba(0,0,0,0.22)"
681
+ };
682
+ function clampInt(val, min, max) {
683
+ if (val === void 0 || val === null || isNaN(val)) return null;
684
+ return Math.max(min, Math.min(max, Math.floor(val)));
685
+ }
686
+ function sanitizeClass(val) {
687
+ if (!val || typeof val !== "string") return "";
688
+ return val.replace(/[^a-zA-Z0-9_\-\s]/g, "").slice(0, 100).trim();
689
+ }
690
+ function isSafeUrl(url) {
691
+ try {
692
+ const parsed = new URL(url, window.location.href);
693
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
694
+ } catch {
695
+ return false;
696
+ }
697
+ }
641
698
  var Banners = class {
642
699
  constructor(client) {
643
700
  this.client = client;
644
701
  this.container = null;
645
702
  this.styleEl = null;
703
+ this.currentAnimation = "slide";
704
+ this.currentStyle = "pinned";
705
+ this.currentPosition = "top";
646
706
  }
647
707
  /** Fetch banner config and show the highest-priority banner */
648
708
  async show(options = {}, userId) {
@@ -660,94 +720,200 @@ var Banners = class {
660
720
  }
661
721
  }
662
722
  if (!banner) return;
663
- this.render(config, banner, options);
723
+ const delay = clampInt(options.delay, 0, 6e4);
724
+ if (delay && delay > 0) {
725
+ setTimeout(() => this.render(config, banner, options), delay);
726
+ } else {
727
+ this.render(config, banner, options);
728
+ }
664
729
  }
665
- /** Remove the banner from the DOM */
730
+ /** Remove the banner from the DOM, respecting the active animation */
666
731
  dismiss() {
667
- if (this.container) {
668
- this.container.classList.remove("tolk-visible");
669
- const pos = this.container.dataset.position || "top";
670
- document.body.style.removeProperty(pos === "top" ? "padding-top" : "padding-bottom");
671
- setTimeout(() => {
672
- this.container?.remove();
673
- this.styleEl?.remove();
732
+ if (!this.container) return;
733
+ const container = this.container;
734
+ const styleEl = this.styleEl;
735
+ container.classList.remove("tolk-visible");
736
+ if (this.currentStyle === "pinned") {
737
+ document.body.style.removeProperty(this.currentPosition === "top" ? "padding-top" : "padding-bottom");
738
+ }
739
+ const cleanup = () => {
740
+ container.remove();
741
+ styleEl?.remove();
742
+ if (this.container === container) {
674
743
  this.container = null;
675
744
  this.styleEl = null;
676
- }, 400);
745
+ }
746
+ };
747
+ if (this.currentAnimation === "none") {
748
+ cleanup();
749
+ } else {
750
+ setTimeout(cleanup, 400);
677
751
  }
678
752
  }
679
753
  render(config, banner, options) {
680
754
  if (this.container) {
681
755
  this.container.remove();
682
756
  this.styleEl?.remove();
757
+ if (this.currentStyle === "pinned") {
758
+ document.body.style.removeProperty(this.currentPosition === "top" ? "padding-top" : "padding-bottom");
759
+ }
760
+ }
761
+ const themeName = options.theme && themes[options.theme] ? options.theme : "light";
762
+ const theme = themes[themeName];
763
+ let position = "top";
764
+ if (options.position === "top" || options.position === "bottom") position = options.position;
765
+ else if (banner.position === "top" || banner.position === "bottom") position = banner.position;
766
+ let resolvedStyle = "pinned";
767
+ if (options.style === "pinned" || options.style === "floating" || options.style === "stacked") {
768
+ resolvedStyle = options.style;
769
+ } else if (banner.style === "pinned" || banner.style === "floating" || banner.style === "stacked") {
770
+ resolvedStyle = banner.style;
683
771
  }
684
- const position = options.position || banner.position || "top";
685
- const bgColor = sanitizeCssColor(banner.background_color) || "#ffffff";
686
- const textColor = sanitizeCssColor(banner.text_color) || "#000000";
772
+ const floating = resolvedStyle === "floating";
773
+ const stacked = resolvedStyle === "stacked";
774
+ let animation = options.animation || "slide";
775
+ if (animation === "pop" && !floating && !stacked) animation = "slide";
776
+ this.currentPosition = position;
777
+ this.currentStyle = resolvedStyle;
778
+ this.currentAnimation = animation;
779
+ const serverBg = sanitizeCssColor(banner.background_color);
780
+ const serverText = sanitizeCssColor(banner.text_color);
781
+ const optBg = sanitizeCssColor(options.bg);
782
+ const optBorder = sanitizeCssColor(options.border);
783
+ const optTitleColor = sanitizeCssColor(options.titleColor);
784
+ const optBodyColor = sanitizeCssColor(options.bodyColor);
785
+ const optCtaBg = sanitizeCssColor(options.ctaBg);
786
+ const optCtaColor = sanitizeCssColor(options.ctaColor);
787
+ const bg = optBg || serverBg || theme.bg;
788
+ const border = optBorder || theme.border;
789
+ const titleColor = optTitleColor || serverText || theme.title.color;
790
+ const bodyColor = optBodyColor || serverText || theme.body.color;
791
+ const ctaBg = optCtaBg || theme.cta.bg || titleColor;
792
+ const ctaColor = optCtaColor || theme.cta.color || bg;
793
+ const titleSize = clampInt(options.titleSize, 10, 24) ?? theme.title.size;
794
+ const titleWeight = options.titleWeight ?? theme.title.weight;
795
+ const bodySize = clampInt(options.bodySize, 10, 20) ?? theme.body.size;
796
+ const bodyWeight = options.bodyWeight ?? theme.body.weight;
797
+ const ctaSize = clampInt(options.ctaSize, 10, 18) ?? theme.cta.size;
798
+ const ctaWeight = options.ctaWeight ?? theme.cta.weight;
799
+ const ctaRadius = clampInt(options.ctaRadius, 0, 100) ?? theme.cta.radius;
800
+ const iconSize = clampInt(options.iconSize, 24, 64) ?? theme.icon.size;
801
+ const iconRadius = clampInt(options.iconRadius, 0, 32) ?? theme.icon.radius;
802
+ const optRadius = clampInt(options.radius, 0, 24);
803
+ const serverRadius = typeof banner.radius === "number" && banner.radius >= 0 && banner.radius <= 24 ? banner.radius : null;
804
+ const optMargin = clampInt(options.margin, 0, 24);
805
+ const serverMargin = typeof banner.margin === "number" && banner.margin >= 0 && banner.margin <= 24 ? banner.margin : null;
806
+ const bannerRadius = floating ? optRadius ?? serverRadius ?? 12 : 0;
807
+ const bannerMargin = floating ? optMargin ?? serverMargin ?? 12 : 0;
808
+ const shadowKey = options.shadow && SHADOW_VALUES[options.shadow] ? options.shadow : banner.shadow && SHADOW_VALUES[banner.shadow] ? banner.shadow : theme.shadow;
809
+ const shadow = SHADOW_VALUES[shadowKey];
810
+ const hideIcon = !!options.hideIcon;
811
+ const hideClose = !!options.hideClose;
812
+ const hideBody = !!options.hideBody;
813
+ const customClass = sanitizeClass(options.customClass);
687
814
  const ctaText = banner.cta_text || "Open";
688
- const baseUrl = this.client.baseUrl;
689
- const installUrl = banner.action_url || baseUrl + (config.install_url || "/install");
815
+ const installUrl = banner.action_url || this.client.baseUrl + (config.install_url || "/install");
690
816
  const safeTop = position === "top" ? "padding-top: env(safe-area-inset-top, 0px);" : "";
691
817
  const safeBottom = position === "bottom" ? "padding-bottom: env(safe-area-inset-bottom, 0px);" : "";
818
+ let containerPosition;
819
+ let containerInsets;
820
+ if (stacked) {
821
+ containerPosition = "position: sticky;";
822
+ containerInsets = `${position}: 0;`;
823
+ } else if (floating) {
824
+ containerPosition = "position: fixed;";
825
+ containerInsets = `left: ${bannerMargin}px; right: ${bannerMargin}px; ${position}: ${bannerMargin}px;`;
826
+ } else {
827
+ containerPosition = "position: fixed;";
828
+ containerInsets = `left: 0; right: 0; ${position}: 0;`;
829
+ }
830
+ const hideOffset = bannerMargin + 60;
831
+ const slideHide = position === "top" ? `translateY(calc(-100% - ${hideOffset}px))` : `translateY(calc(100% + ${hideOffset}px))`;
832
+ let hiddenCss = "";
833
+ let visibleCss = "";
834
+ let transitionCss = "";
835
+ if (animation === "slide") {
836
+ hiddenCss = `transform: ${slideHide};`;
837
+ visibleCss = "transform: translateY(0);";
838
+ transitionCss = "transition: transform 0.35s ease;";
839
+ } else if (animation === "fade") {
840
+ hiddenCss = "opacity: 0;";
841
+ visibleCss = "opacity: 1;";
842
+ transitionCss = "transition: opacity 0.3s ease;";
843
+ } else if (animation === "pop") {
844
+ hiddenCss = "opacity: 0; transform: scale(0.92);";
845
+ visibleCss = "opacity: 1; transform: scale(1);";
846
+ transitionCss = "transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.25s ease;";
847
+ }
692
848
  const container = document.createElement("div");
693
849
  container.id = "tolinku-banner";
694
850
  container.setAttribute("role", "banner");
695
851
  container.setAttribute("aria-live", "polite");
696
852
  container.dataset.position = position;
853
+ if (customClass) container.className = customClass;
854
+ const borderCss = border ? `border: 1px solid ${border};` : "";
855
+ const radiusCss = bannerRadius > 0 ? `border-radius: ${bannerRadius}px;` : "";
697
856
  const style = document.createElement("style");
698
857
  style.textContent = `
699
858
  #tolinku-banner {
700
- position: fixed;
701
- ${position}: 0;
702
- left: 0; right: 0;
859
+ ${containerPosition}
860
+ ${containerInsets}
703
861
  z-index: 999999;
704
862
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
705
- transform: translateY(${position === "top" ? "-100%" : "100%"});
706
- transition: transform 0.35s ease;
863
+ ${hiddenCss}
864
+ ${transitionCss}
707
865
  ${safeTop}${safeBottom}
708
866
  }
709
- #tolinku-banner.tolk-visible { transform: translateY(0); }
867
+ #tolinku-banner.tolk-visible { ${visibleCss} }
710
868
  #tolinku-banner .tolk-inner {
711
869
  display: flex; align-items: center; gap: 10px;
712
870
  padding: 10px 14px;
713
- background: ${bgColor}; color: ${textColor};
714
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
871
+ background: ${bg};
872
+ ${borderCss}
873
+ ${radiusCss}
874
+ box-shadow: ${shadow};
715
875
  }
716
876
  #tolinku-banner .tolk-close {
717
877
  background: none; border: none; font-size: 20px; line-height: 1;
718
- cursor: pointer; color: ${textColor}; opacity: 0.6; padding: 0 4px; flex-shrink: 0;
878
+ cursor: pointer; color: ${titleColor}; opacity: 0.6; padding: 0 4px; flex-shrink: 0;
719
879
  }
720
880
  #tolinku-banner .tolk-close:hover { opacity: 1; }
721
881
  #tolinku-banner .tolk-icon {
722
- width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0; object-fit: cover;
882
+ width: ${iconSize}px; height: ${iconSize}px; border-radius: ${iconRadius}px;
883
+ flex-shrink: 0; object-fit: cover;
723
884
  }
724
885
  #tolinku-banner .tolk-text { flex: 1; min-width: 0; }
725
886
  #tolinku-banner .tolk-title {
726
- font-size: 14px; font-weight: 600; margin: 0;
887
+ font-size: ${titleSize}px; font-weight: ${titleWeight}; line-height: 1.3;
888
+ color: ${titleColor}; margin: 0;
727
889
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
728
890
  }
729
891
  #tolinku-banner .tolk-body {
730
- font-size: 12px; margin: 0; opacity: 0.75;
892
+ font-size: ${bodySize}px; font-weight: ${bodyWeight}; line-height: 1.3;
893
+ color: ${bodyColor}; margin: 0; opacity: 0.75;
731
894
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
732
895
  }
733
896
  #tolinku-banner .tolk-cta {
734
- display: inline-block; padding: 6px 16px; border-radius: 100px;
735
- font-size: 13px; font-weight: 600; text-decoration: none;
736
- background: ${textColor}; color: ${bgColor}; flex-shrink: 0; text-align: center;
897
+ display: inline-block; padding: 0.5em 1.2em; border-radius: ${ctaRadius}px;
898
+ font-size: ${ctaSize}px; font-weight: ${ctaWeight}; line-height: 1.2;
899
+ text-decoration: none; background: ${ctaBg}; color: ${ctaColor};
900
+ flex-shrink: 0; text-align: center;
737
901
  }
738
902
  `;
739
903
  const inner = document.createElement("div");
740
904
  inner.className = "tolk-inner";
741
- const closeBtn = document.createElement("button");
742
- closeBtn.className = "tolk-close";
743
- closeBtn.setAttribute("aria-label", "Dismiss banner");
744
- closeBtn.textContent = "\xD7";
745
- closeBtn.addEventListener("click", () => {
746
- saveBannerDismissal(banner.id);
747
- this.dismiss();
748
- });
749
- inner.appendChild(closeBtn);
750
- if (config.app_icon && isSafeUrl(config.app_icon)) {
905
+ if (!hideClose) {
906
+ const closeBtn = document.createElement("button");
907
+ closeBtn.className = "tolk-close";
908
+ closeBtn.setAttribute("aria-label", "Dismiss banner");
909
+ closeBtn.textContent = "\xD7";
910
+ closeBtn.addEventListener("click", () => {
911
+ saveBannerDismissal(banner.id);
912
+ this.dismiss();
913
+ });
914
+ inner.appendChild(closeBtn);
915
+ }
916
+ if (!hideIcon && config.app_icon && isSafeUrl(config.app_icon)) {
751
917
  const icon = document.createElement("img");
752
918
  icon.className = "tolk-icon";
753
919
  icon.src = config.app_icon;
@@ -760,7 +926,7 @@ var Banners = class {
760
926
  titleEl.className = "tolk-title";
761
927
  titleEl.textContent = banner.title || config.app_name || "Get the App";
762
928
  textWrap.appendChild(titleEl);
763
- if (banner.body) {
929
+ if (!hideBody && banner.body) {
764
930
  const bodyEl = document.createElement("p");
765
931
  bodyEl.className = "tolk-body";
766
932
  bodyEl.textContent = banner.body;
@@ -774,29 +940,57 @@ var Banners = class {
774
940
  inner.appendChild(cta);
775
941
  container.appendChild(inner);
776
942
  document.head.appendChild(style);
777
- document.body.appendChild(container);
943
+ if (stacked) {
944
+ const anchor = findStackedAnchor(position, options.anchor);
945
+ if (anchor && anchor.parentNode) {
946
+ if (position === "top") {
947
+ anchor.parentNode.insertBefore(container, anchor);
948
+ } else if (anchor.nextSibling) {
949
+ anchor.parentNode.insertBefore(container, anchor.nextSibling);
950
+ } else {
951
+ anchor.parentNode.appendChild(container);
952
+ }
953
+ } else if (document.body) {
954
+ if (position === "top") {
955
+ document.body.insertBefore(container, document.body.firstChild);
956
+ } else {
957
+ document.body.appendChild(container);
958
+ }
959
+ }
960
+ } else {
961
+ document.body.appendChild(container);
962
+ }
778
963
  this.container = container;
779
964
  this.styleEl = style;
780
965
  requestAnimationFrame(() => {
781
966
  requestAnimationFrame(() => {
782
967
  container.classList.add("tolk-visible");
783
- const bannerHeight = container.offsetHeight + "px";
784
- if (position === "top") {
785
- document.body.style.paddingTop = bannerHeight;
786
- } else {
787
- document.body.style.paddingBottom = bannerHeight;
968
+ if (!floating && !stacked) {
969
+ const bannerHeight = container.offsetHeight + "px";
970
+ if (position === "top") {
971
+ document.body.style.paddingTop = bannerHeight;
972
+ } else {
973
+ document.body.style.paddingBottom = bannerHeight;
974
+ }
788
975
  }
789
976
  });
790
977
  });
791
978
  }
792
979
  };
793
- function isSafeUrl(url) {
794
- try {
795
- const parsed = new URL(url, window.location.href);
796
- return parsed.protocol === "http:" || parsed.protocol === "https:";
797
- } catch {
798
- return false;
980
+ function findStackedAnchor(position, explicit) {
981
+ if (explicit) {
982
+ try {
983
+ const el = document.querySelector(explicit);
984
+ if (el) return el;
985
+ } catch {
986
+ }
987
+ }
988
+ const selectors = position === "top" ? ["header", '[role="banner"]', "nav", ".header", "#header", ".navbar"] : ["footer", '[role="contentinfo"]', ".footer", "#footer"];
989
+ for (const s of selectors) {
990
+ const el = document.body?.querySelector(s);
991
+ if (el) return el;
799
992
  }
993
+ return null;
800
994
  }
801
995
 
802
996
  // src/messages.ts