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