@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/README.md +7 -2
- package/dist/index.d.mts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +248 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +248 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
}
|
|
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
|
|
685
|
-
const
|
|
686
|
-
|
|
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
|
|
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
|
-
|
|
701
|
-
${
|
|
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
|
-
|
|
706
|
-
|
|
863
|
+
${hiddenCss}
|
|
864
|
+
${transitionCss}
|
|
707
865
|
${safeTop}${safeBottom}
|
|
708
866
|
}
|
|
709
|
-
#tolinku-banner.tolk-visible {
|
|
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: ${
|
|
714
|
-
|
|
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: ${
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
735
|
-
font-size:
|
|
736
|
-
background: ${
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|