@zerocost/sdk 0.14.0 → 0.16.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.
@@ -1,2 +1,3 @@
1
1
  export declare const EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
2
+ export { ZEROCOST_BASE_URL } from './constants';
2
3
  export declare function getBaseUrl(custom?: string): string;
@@ -1,12 +1,3 @@
1
- /**
2
- * consent-ui.ts — Pure DOM consent popup for the Zerocost SDK.
3
- *
4
- * - Desktop: centered card modal (max 480px)
5
- * - Mobile (≤640px): bottom-sheet modal
6
- * - Themes: light / dark / auto
7
- * - Non-dismissable (no Escape, no backdrop click)
8
- * - Returns a Promise that resolves with the user's toggle selections
9
- */
10
1
  export interface ConsentUIOptions {
11
2
  appName: string;
12
3
  theme: 'light' | 'dark' | 'auto';
@@ -34,4 +34,6 @@ export declare class ConsentManager {
34
34
  private getOrCreateUserId;
35
35
  private generateUUID;
36
36
  private submitToServer;
37
+ private injectSettingsButton;
38
+ private removeSettingsButton;
37
39
  }
@@ -0,0 +1,4 @@
1
+ export declare const ZEROCOST_DOMAINS: {
2
+ MAIN: string;
3
+ };
4
+ export declare const ZEROCOST_BASE_URL: string;
package/dist/index.cjs CHANGED
@@ -28,6 +28,14 @@ __export(index_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(index_exports);
30
30
 
31
+ // src/core/constants.ts
32
+ var ZEROCOST_DOMAINS = {
33
+ MAIN: "https://zerocost.lovable.app"
34
+ //APP: 'https://app.zerocost.com',
35
+ //DOCS: 'https://docs.zerocost.com',
36
+ };
37
+ var ZEROCOST_BASE_URL = ZEROCOST_DOMAINS.MAIN;
38
+
31
39
  // src/core/config.ts
32
40
  var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
33
41
  function getBaseUrl(custom) {
@@ -70,7 +78,7 @@ var ZerocostClient = class {
70
78
  ...body || {},
71
79
  app_id: this.config.appId
72
80
  };
73
- this.log(`\u2192 ${url}`, payload);
81
+ this.log(`\u2192 ${path}`, payload);
74
82
  const res = await fetch(url, {
75
83
  method: "POST",
76
84
  headers: {
@@ -89,7 +97,8 @@ var ZerocostClient = class {
89
97
  }
90
98
  log(message, data) {
91
99
  if (this.config.debug) {
92
- console.log(`[Zerocost] ${message}`, data ?? "");
100
+ const sanitizedMessage = typeof message === "string" ? message.replace(/https:\/\/[a-z0-9.-]+\.supabase\.co/gi, "[INFRA]") : message;
101
+ console.log(`[Zerocost] ${sanitizedMessage}`, data ?? "");
93
102
  }
94
103
  }
95
104
  };
@@ -141,6 +150,12 @@ var TrackModule = class {
141
150
 
142
151
  // src/core/widget-render.ts
143
152
  var SDK_WIDGET_REFRESH_MS = 2e4;
153
+ var CLOSE_ICON_SVG = `
154
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;">
155
+ <line x1="18" y1="6" x2="6" y2="18"></line>
156
+ <line x1="6" y1="6" x2="18" y2="18"></line>
157
+ </svg>
158
+ `;
144
159
  function escapeHtml(value) {
145
160
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
146
161
  }
@@ -194,7 +209,7 @@ function renderVideoWidget(ad, theme) {
194
209
  <div style="position:relative;width:200px;aspect-ratio:9/16;border-radius:16px;overflow:hidden;background:${palette.bg};border:1px solid rgba(255,255,255,0.12);box-shadow:0 20px 60px rgba(0,0,0,0.45);font-family:Space Grotesk,system-ui,sans-serif;">
195
210
  ${media}
196
211
  <div style="position:absolute;top:10px;right:10px;display:flex;gap:8px;z-index:3;">
197
- <button type="button" data-zc-close style="width:22px;height:22px;border:none;border-radius:999px;background:rgba(0,0,0,0.52);color:#fff;font-size:12px;cursor:pointer;">x</button>
212
+ <button type="button" data-zc-close style="width:24px;height:24px;padding:5px;border:none;border-radius:999px;background:rgba(0,0,0,0.52);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
198
213
  </div>
199
214
  <div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0.04) 0%,rgba(0,0,0,0.12) 35%,rgba(0,0,0,0.85) 100%);"></div>
200
215
  <div style="position:absolute;left:0;right:0;bottom:0;padding:14px;z-index:2;">
@@ -215,7 +230,7 @@ function renderTooltipWidget(ad, theme) {
215
230
  <span style="width:6px;height:6px;border-radius:999px;background:${palette.text};display:inline-block;"></span>
216
231
  Sponsored
217
232
  </div>
218
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
233
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
219
234
  </div>
220
235
  <div style="margin-top:10px;color:${palette.text};font-size:13px;line-height:1.55;">
221
236
  ${escapeHtml(ad.description || ad.title)} <a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="color:${palette.text};font-weight:700;text-decoration:underline;text-underline-offset:2px;">${escapeHtml(ad.cta_text || "Learn More")}</a>
@@ -232,7 +247,7 @@ function renderSponsoredCard(ad, theme) {
232
247
  <div style="padding:12px;">
233
248
  <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
234
249
  <div style="display:inline-flex;align-items:center;padding:4px 6px;border-radius:999px;background:${palette.badgeBg};color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</div>
235
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
250
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
236
251
  </div>
237
252
  <div style="margin-top:10px;color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
238
253
  ${ad.description ? `<div style="margin-top:6px;color:${palette.textMuted};font-size:11px;line-height:1.35;">${escapeHtml(ad.description)}</div>` : ""}
@@ -248,7 +263,7 @@ function renderInlineText(ad, theme) {
248
263
  <div style="margin:10px 0;border-radius:14px;overflow:hidden;background:${palette.surface};border:1px solid ${palette.border};font-family:Space Grotesk,system-ui,sans-serif;">
249
264
  <div style="display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-bottom:1px solid ${palette.border};background:${palette.surfaceStrong};">
250
265
  <span style="color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</span>
251
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
266
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
252
267
  </div>
253
268
  <div style="padding:12px;display:flex;gap:10px;align-items:flex-start;">
254
269
  ${media}
@@ -277,15 +292,15 @@ function renderWidgetMarkup(ad, options) {
277
292
 
278
293
  // src/modules/widget.ts
279
294
  var POSITION_STYLES = {
280
- "bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
295
+ "bottom-right": "position:fixed;bottom:24px;right:40px;z-index:9999;",
281
296
  "bottom-left": "position:fixed;bottom:24px;left:80px;z-index:9999;",
282
- "top-right": "position:fixed;top:24px;right:24px;z-index:9999;",
297
+ "top-right": "position:fixed;top:24px;right:40px;z-index:9999;",
283
298
  "top-left": "position:fixed;top:24px;left:24px;z-index:9999;",
284
299
  "bottom-center": "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;",
285
300
  "top-center": "position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:9999;",
286
301
  "center": "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;",
287
302
  "sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
288
- "sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
303
+ "sidebar-right": "position:fixed;top:50%;right:40px;transform:translateY(-50%);z-index:9999;"
289
304
  };
290
305
  var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
291
306
  var AUTO_SLOT_ID = "zerocost-auto-slot";
@@ -1589,6 +1604,44 @@ function injectStyles(theme) {
1589
1604
  .zc-consent-confirm:active {
1590
1605
  opacity: 0.75;
1591
1606
  }
1607
+
1608
+ /* Floating settings button */
1609
+ .zc-settings-btn {
1610
+ position: fixed;
1611
+ width: 44px;
1612
+ height: 44px;
1613
+ border-radius: 50%;
1614
+ background: var(--zc-bg);
1615
+ border: 1px solid var(--zc-border);
1616
+ color: var(--zc-text);
1617
+ display: flex;
1618
+ align-items: center;
1619
+ justify-content: center;
1620
+ cursor: pointer;
1621
+ z-index: 999998;
1622
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1623
+ transition: transform 200ms, border-color 200ms;
1624
+ }
1625
+
1626
+ .zc-settings-btn:hover {
1627
+ transform: scale(1.05);
1628
+ border-color: var(--zc-text-secondary);
1629
+ }
1630
+
1631
+ .zc-settings-btn svg {
1632
+ width: 20px;
1633
+ height: 20px;
1634
+ }
1635
+
1636
+ .zc-settings-bottom-left { bottom: 20px; left: 20px; }
1637
+ .zc-settings-bottom-right { bottom: 20px; right: 20px; }
1638
+ .zc-settings-top-left { top: 20px; left: 20px; }
1639
+ .zc-settings-top-right { top: 20px; right: 20px; }
1640
+
1641
+ @media (max-width: 640px) {
1642
+ .zc-settings-btn { width: 40px; height: 40px; }
1643
+ .zc-settings-bottom-left, .zc-settings-bottom-right { bottom: 16px; }
1644
+ }
1592
1645
  `;
1593
1646
  const style = document.createElement("style");
1594
1647
  style.id = STYLE_ID;
@@ -1671,33 +1724,33 @@ function showConsentUI(options) {
1671
1724
  card.appendChild(subtitle);
1672
1725
  const toggles = document.createElement("div");
1673
1726
  toggles.className = "zc-consent-toggles";
1674
- const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
1727
+ const zerocostBaseUrl = ZEROCOST_BASE_URL;
1675
1728
  toggles.appendChild(createToggleCard(
1676
1729
  "zc-toggle-ads",
1677
1730
  "Ads",
1678
1731
  "Contextual, non-intrusive ads. No cookies or browsing history used.",
1679
- `${baseUrl}/consent/ads`,
1732
+ `${zerocostBaseUrl}/docs/ads`,
1680
1733
  defaults.ads
1681
1734
  ));
1682
1735
  toggles.appendChild(createToggleCard(
1683
1736
  "zc-toggle-usage",
1684
1737
  "Usage data",
1685
1738
  "Anonymized usage patterns. No personal information is shared.",
1686
- `${baseUrl}/consent/usage-data`,
1739
+ `${zerocostBaseUrl}/docs/usage-data`,
1687
1740
  defaults.usageData
1688
1741
  ));
1689
1742
  toggles.appendChild(createToggleCard(
1690
1743
  "zc-toggle-ai",
1691
1744
  "AI interactions",
1692
1745
  "Anonymized conversation data used for AI research.",
1693
- `${baseUrl}/consent/ai-interactions`,
1746
+ `${zerocostBaseUrl}/docs/ai-interactions`,
1694
1747
  defaults.aiInteractions
1695
1748
  ));
1696
1749
  card.appendChild(toggles);
1697
1750
  const footer = document.createElement("div");
1698
1751
  footer.className = "zc-consent-footer";
1699
1752
  const ppLink = document.createElement("a");
1700
- ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
1753
+ ppLink.href = privacyPolicyUrl || `${zerocostBaseUrl}/docs/privacy`;
1701
1754
  ppLink.target = "_blank";
1702
1755
  ppLink.rel = "noopener noreferrer";
1703
1756
  ppLink.textContent = "Privacy Policy";
@@ -1707,7 +1760,7 @@ function showConsentUI(options) {
1707
1760
  sep1.textContent = "\xB7";
1708
1761
  footer.appendChild(sep1);
1709
1762
  const termsLink = document.createElement("a");
1710
- termsLink.href = `${baseUrl}/terms`;
1763
+ termsLink.href = `${zerocostBaseUrl}/docs/terms`;
1711
1764
  termsLink.target = "_blank";
1712
1765
  termsLink.rel = "noopener noreferrer";
1713
1766
  termsLink.textContent = "Terms";
@@ -1717,7 +1770,7 @@ function showConsentUI(options) {
1717
1770
  sep2.textContent = "\xB7";
1718
1771
  footer.appendChild(sep2);
1719
1772
  const dnsLink = document.createElement("a");
1720
- dnsLink.href = `${baseUrl}/do-not-sell`;
1773
+ dnsLink.href = `${zerocostBaseUrl}/docs/do-not-sell`;
1721
1774
  dnsLink.target = "_blank";
1722
1775
  dnsLink.rel = "noopener noreferrer";
1723
1776
  dnsLink.textContent = "Do Not Sell My Data";
@@ -1761,6 +1814,9 @@ var ConsentManager = class {
1761
1814
  this.appName = opts.appName ?? "";
1762
1815
  this.theme = opts.theme ?? "dark";
1763
1816
  this.hydrateFromStorage();
1817
+ if (this.consentConfig.showSettingsButton) {
1818
+ this.injectSettingsButton();
1819
+ }
1764
1820
  }
1765
1821
  // ── Public API (per spec §6.3) ───────────────────────────────────
1766
1822
  /** Returns the current consent record, or null if none exists. */
@@ -1826,6 +1882,9 @@ var ConsentManager = class {
1826
1882
  };
1827
1883
  this.record = record;
1828
1884
  this.writeStorage(record);
1885
+ if (this.consentConfig.showSettingsButton) {
1886
+ this.injectSettingsButton();
1887
+ }
1829
1888
  if (this.consentConfig.onConsentChange) {
1830
1889
  try {
1831
1890
  this.consentConfig.onConsentChange(record);
@@ -1904,6 +1963,23 @@ var ConsentManager = class {
1904
1963
  this.client.log(`Failed to submit consent to server: ${err}`);
1905
1964
  }
1906
1965
  }
1966
+ injectSettingsButton() {
1967
+ if (typeof document === "undefined") return;
1968
+ const existing = document.getElementById("zerocost-privacy-settings-btn");
1969
+ if (existing) return;
1970
+ const btn = document.createElement("button");
1971
+ btn.id = "zerocost-privacy-settings-btn";
1972
+ btn.setAttribute("aria-label", "Privacy Settings");
1973
+ btn.title = "Privacy Settings";
1974
+ const pos = this.consentConfig.buttonPosition || "bottom-left";
1975
+ btn.className = `zc-settings-btn zc-settings-${pos}`;
1976
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
1977
+ btn.addEventListener("click", () => this.open());
1978
+ document.body.appendChild(btn);
1979
+ }
1980
+ removeSettingsButton() {
1981
+ document.getElementById("zerocost-privacy-settings-btn")?.remove();
1982
+ }
1907
1983
  };
1908
1984
 
1909
1985
  // src/index.ts
@@ -1937,21 +2013,15 @@ var ZerocostSDK = class {
1937
2013
  });
1938
2014
  }
1939
2015
  async init() {
1940
- this.core.init();
1941
2016
  if (typeof document === "undefined") {
1942
- this.core.log("Running in non-browser environment; skipping DOM injection.");
1943
2017
  return;
1944
2018
  }
1945
2019
  if (window !== window.top) {
1946
- this.core.log("Running inside an iframe. Ads render if permissions allow.");
1947
2020
  }
1948
- this.core.log("Initializing Zerocost SDK.");
1949
2021
  if (this.consent.shouldPrompt()) {
1950
- this.core.log("Consent required \u2014 showing prompt.");
1951
2022
  await this.consent.promptAndWait();
1952
2023
  }
1953
2024
  if (!this.consent.has("ads")) {
1954
- this.core.log("Ads consent not granted \u2014 skipping ad injection.");
1955
2025
  return;
1956
2026
  }
1957
2027
  const cachedConfig = this.readCachedConfig();
@@ -1959,12 +2029,11 @@ var ZerocostSDK = class {
1959
2029
  this.lastConfigHash = this.configToHash(cachedConfig);
1960
2030
  this.syncDataCollection(cachedConfig.dataCollection);
1961
2031
  await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
1962
- this.core.log("Applied cached config immediately.");
1963
2032
  }
1964
2033
  this.startConfigSync();
1965
2034
  try {
1966
2035
  await this.refreshConfig({ force: true, reason: "init" });
1967
- this.core.log("SDK fully initialized. Ads are rendering automatically.");
2036
+ this.core.log(`Zerocost SDK initialized (${this.core.getConfig().appId})`);
1968
2037
  } catch (err) {
1969
2038
  this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
1970
2039
  await this.widget.autoInject();
@@ -2138,6 +2207,10 @@ var ZerocostSDK = class {
2138
2207
  return { valid: false, error: err.message };
2139
2208
  }
2140
2209
  }
2210
+ /** Open the consent/privacy settings popup. */
2211
+ async showSettings() {
2212
+ return this.consent.open();
2213
+ }
2141
2214
  };
2142
2215
  // Annotate the CommonJS export names for ESM import in node:
2143
2216
  0 && (module.exports = {
package/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare class ZerocostSDK {
39
39
  valid: boolean;
40
40
  error?: string;
41
41
  }>;
42
+ /** Open the consent/privacy settings popup. */
43
+ showSettings(): Promise<void>;
42
44
  }
43
45
  export * from './types';
44
46
  export { ZerocostClient } from './core/client';
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ // src/core/constants.ts
2
+ var ZEROCOST_DOMAINS = {
3
+ MAIN: "https://zerocost.lovable.app"
4
+ //APP: 'https://app.zerocost.com',
5
+ //DOCS: 'https://docs.zerocost.com',
6
+ };
7
+ var ZEROCOST_BASE_URL = ZEROCOST_DOMAINS.MAIN;
8
+
1
9
  // src/core/config.ts
2
10
  var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
3
11
  function getBaseUrl(custom) {
@@ -40,7 +48,7 @@ var ZerocostClient = class {
40
48
  ...body || {},
41
49
  app_id: this.config.appId
42
50
  };
43
- this.log(`\u2192 ${url}`, payload);
51
+ this.log(`\u2192 ${path}`, payload);
44
52
  const res = await fetch(url, {
45
53
  method: "POST",
46
54
  headers: {
@@ -59,7 +67,8 @@ var ZerocostClient = class {
59
67
  }
60
68
  log(message, data) {
61
69
  if (this.config.debug) {
62
- console.log(`[Zerocost] ${message}`, data ?? "");
70
+ const sanitizedMessage = typeof message === "string" ? message.replace(/https:\/\/[a-z0-9.-]+\.supabase\.co/gi, "[INFRA]") : message;
71
+ console.log(`[Zerocost] ${sanitizedMessage}`, data ?? "");
63
72
  }
64
73
  }
65
74
  };
@@ -111,6 +120,12 @@ var TrackModule = class {
111
120
 
112
121
  // src/core/widget-render.ts
113
122
  var SDK_WIDGET_REFRESH_MS = 2e4;
123
+ var CLOSE_ICON_SVG = `
124
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;">
125
+ <line x1="18" y1="6" x2="6" y2="18"></line>
126
+ <line x1="6" y1="6" x2="18" y2="18"></line>
127
+ </svg>
128
+ `;
114
129
  function escapeHtml(value) {
115
130
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
116
131
  }
@@ -164,7 +179,7 @@ function renderVideoWidget(ad, theme) {
164
179
  <div style="position:relative;width:200px;aspect-ratio:9/16;border-radius:16px;overflow:hidden;background:${palette.bg};border:1px solid rgba(255,255,255,0.12);box-shadow:0 20px 60px rgba(0,0,0,0.45);font-family:Space Grotesk,system-ui,sans-serif;">
165
180
  ${media}
166
181
  <div style="position:absolute;top:10px;right:10px;display:flex;gap:8px;z-index:3;">
167
- <button type="button" data-zc-close style="width:22px;height:22px;border:none;border-radius:999px;background:rgba(0,0,0,0.52);color:#fff;font-size:12px;cursor:pointer;">x</button>
182
+ <button type="button" data-zc-close style="width:24px;height:24px;padding:5px;border:none;border-radius:999px;background:rgba(0,0,0,0.52);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
168
183
  </div>
169
184
  <div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0.04) 0%,rgba(0,0,0,0.12) 35%,rgba(0,0,0,0.85) 100%);"></div>
170
185
  <div style="position:absolute;left:0;right:0;bottom:0;padding:14px;z-index:2;">
@@ -185,7 +200,7 @@ function renderTooltipWidget(ad, theme) {
185
200
  <span style="width:6px;height:6px;border-radius:999px;background:${palette.text};display:inline-block;"></span>
186
201
  Sponsored
187
202
  </div>
188
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
203
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
189
204
  </div>
190
205
  <div style="margin-top:10px;color:${palette.text};font-size:13px;line-height:1.55;">
191
206
  ${escapeHtml(ad.description || ad.title)} <a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="color:${palette.text};font-weight:700;text-decoration:underline;text-underline-offset:2px;">${escapeHtml(ad.cta_text || "Learn More")}</a>
@@ -202,7 +217,7 @@ function renderSponsoredCard(ad, theme) {
202
217
  <div style="padding:12px;">
203
218
  <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
204
219
  <div style="display:inline-flex;align-items:center;padding:4px 6px;border-radius:999px;background:${palette.badgeBg};color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</div>
205
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
220
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
206
221
  </div>
207
222
  <div style="margin-top:10px;color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
208
223
  ${ad.description ? `<div style="margin-top:6px;color:${palette.textMuted};font-size:11px;line-height:1.35;">${escapeHtml(ad.description)}</div>` : ""}
@@ -218,7 +233,7 @@ function renderInlineText(ad, theme) {
218
233
  <div style="margin:10px 0;border-radius:14px;overflow:hidden;background:${palette.surface};border:1px solid ${palette.border};font-family:Space Grotesk,system-ui,sans-serif;">
219
234
  <div style="display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-bottom:1px solid ${palette.border};background:${palette.surfaceStrong};">
220
235
  <span style="color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</span>
221
- <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
236
+ <button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};width:16px;height:16px;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;">${CLOSE_ICON_SVG}</button>
222
237
  </div>
223
238
  <div style="padding:12px;display:flex;gap:10px;align-items:flex-start;">
224
239
  ${media}
@@ -247,15 +262,15 @@ function renderWidgetMarkup(ad, options) {
247
262
 
248
263
  // src/modules/widget.ts
249
264
  var POSITION_STYLES = {
250
- "bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
265
+ "bottom-right": "position:fixed;bottom:24px;right:40px;z-index:9999;",
251
266
  "bottom-left": "position:fixed;bottom:24px;left:80px;z-index:9999;",
252
- "top-right": "position:fixed;top:24px;right:24px;z-index:9999;",
267
+ "top-right": "position:fixed;top:24px;right:40px;z-index:9999;",
253
268
  "top-left": "position:fixed;top:24px;left:24px;z-index:9999;",
254
269
  "bottom-center": "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;",
255
270
  "top-center": "position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:9999;",
256
271
  "center": "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;",
257
272
  "sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
258
- "sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
273
+ "sidebar-right": "position:fixed;top:50%;right:40px;transform:translateY(-50%);z-index:9999;"
259
274
  };
260
275
  var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
261
276
  var AUTO_SLOT_ID = "zerocost-auto-slot";
@@ -1559,6 +1574,44 @@ function injectStyles(theme) {
1559
1574
  .zc-consent-confirm:active {
1560
1575
  opacity: 0.75;
1561
1576
  }
1577
+
1578
+ /* Floating settings button */
1579
+ .zc-settings-btn {
1580
+ position: fixed;
1581
+ width: 44px;
1582
+ height: 44px;
1583
+ border-radius: 50%;
1584
+ background: var(--zc-bg);
1585
+ border: 1px solid var(--zc-border);
1586
+ color: var(--zc-text);
1587
+ display: flex;
1588
+ align-items: center;
1589
+ justify-content: center;
1590
+ cursor: pointer;
1591
+ z-index: 999998;
1592
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1593
+ transition: transform 200ms, border-color 200ms;
1594
+ }
1595
+
1596
+ .zc-settings-btn:hover {
1597
+ transform: scale(1.05);
1598
+ border-color: var(--zc-text-secondary);
1599
+ }
1600
+
1601
+ .zc-settings-btn svg {
1602
+ width: 20px;
1603
+ height: 20px;
1604
+ }
1605
+
1606
+ .zc-settings-bottom-left { bottom: 20px; left: 20px; }
1607
+ .zc-settings-bottom-right { bottom: 20px; right: 20px; }
1608
+ .zc-settings-top-left { top: 20px; left: 20px; }
1609
+ .zc-settings-top-right { top: 20px; right: 20px; }
1610
+
1611
+ @media (max-width: 640px) {
1612
+ .zc-settings-btn { width: 40px; height: 40px; }
1613
+ .zc-settings-bottom-left, .zc-settings-bottom-right { bottom: 16px; }
1614
+ }
1562
1615
  `;
1563
1616
  const style = document.createElement("style");
1564
1617
  style.id = STYLE_ID;
@@ -1641,33 +1694,33 @@ function showConsentUI(options) {
1641
1694
  card.appendChild(subtitle);
1642
1695
  const toggles = document.createElement("div");
1643
1696
  toggles.className = "zc-consent-toggles";
1644
- const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
1697
+ const zerocostBaseUrl = ZEROCOST_BASE_URL;
1645
1698
  toggles.appendChild(createToggleCard(
1646
1699
  "zc-toggle-ads",
1647
1700
  "Ads",
1648
1701
  "Contextual, non-intrusive ads. No cookies or browsing history used.",
1649
- `${baseUrl}/consent/ads`,
1702
+ `${zerocostBaseUrl}/docs/ads`,
1650
1703
  defaults.ads
1651
1704
  ));
1652
1705
  toggles.appendChild(createToggleCard(
1653
1706
  "zc-toggle-usage",
1654
1707
  "Usage data",
1655
1708
  "Anonymized usage patterns. No personal information is shared.",
1656
- `${baseUrl}/consent/usage-data`,
1709
+ `${zerocostBaseUrl}/docs/usage-data`,
1657
1710
  defaults.usageData
1658
1711
  ));
1659
1712
  toggles.appendChild(createToggleCard(
1660
1713
  "zc-toggle-ai",
1661
1714
  "AI interactions",
1662
1715
  "Anonymized conversation data used for AI research.",
1663
- `${baseUrl}/consent/ai-interactions`,
1716
+ `${zerocostBaseUrl}/docs/ai-interactions`,
1664
1717
  defaults.aiInteractions
1665
1718
  ));
1666
1719
  card.appendChild(toggles);
1667
1720
  const footer = document.createElement("div");
1668
1721
  footer.className = "zc-consent-footer";
1669
1722
  const ppLink = document.createElement("a");
1670
- ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
1723
+ ppLink.href = privacyPolicyUrl || `${zerocostBaseUrl}/docs/privacy`;
1671
1724
  ppLink.target = "_blank";
1672
1725
  ppLink.rel = "noopener noreferrer";
1673
1726
  ppLink.textContent = "Privacy Policy";
@@ -1677,7 +1730,7 @@ function showConsentUI(options) {
1677
1730
  sep1.textContent = "\xB7";
1678
1731
  footer.appendChild(sep1);
1679
1732
  const termsLink = document.createElement("a");
1680
- termsLink.href = `${baseUrl}/terms`;
1733
+ termsLink.href = `${zerocostBaseUrl}/docs/terms`;
1681
1734
  termsLink.target = "_blank";
1682
1735
  termsLink.rel = "noopener noreferrer";
1683
1736
  termsLink.textContent = "Terms";
@@ -1687,7 +1740,7 @@ function showConsentUI(options) {
1687
1740
  sep2.textContent = "\xB7";
1688
1741
  footer.appendChild(sep2);
1689
1742
  const dnsLink = document.createElement("a");
1690
- dnsLink.href = `${baseUrl}/do-not-sell`;
1743
+ dnsLink.href = `${zerocostBaseUrl}/docs/do-not-sell`;
1691
1744
  dnsLink.target = "_blank";
1692
1745
  dnsLink.rel = "noopener noreferrer";
1693
1746
  dnsLink.textContent = "Do Not Sell My Data";
@@ -1731,6 +1784,9 @@ var ConsentManager = class {
1731
1784
  this.appName = opts.appName ?? "";
1732
1785
  this.theme = opts.theme ?? "dark";
1733
1786
  this.hydrateFromStorage();
1787
+ if (this.consentConfig.showSettingsButton) {
1788
+ this.injectSettingsButton();
1789
+ }
1734
1790
  }
1735
1791
  // ── Public API (per spec §6.3) ───────────────────────────────────
1736
1792
  /** Returns the current consent record, or null if none exists. */
@@ -1796,6 +1852,9 @@ var ConsentManager = class {
1796
1852
  };
1797
1853
  this.record = record;
1798
1854
  this.writeStorage(record);
1855
+ if (this.consentConfig.showSettingsButton) {
1856
+ this.injectSettingsButton();
1857
+ }
1799
1858
  if (this.consentConfig.onConsentChange) {
1800
1859
  try {
1801
1860
  this.consentConfig.onConsentChange(record);
@@ -1874,6 +1933,23 @@ var ConsentManager = class {
1874
1933
  this.client.log(`Failed to submit consent to server: ${err}`);
1875
1934
  }
1876
1935
  }
1936
+ injectSettingsButton() {
1937
+ if (typeof document === "undefined") return;
1938
+ const existing = document.getElementById("zerocost-privacy-settings-btn");
1939
+ if (existing) return;
1940
+ const btn = document.createElement("button");
1941
+ btn.id = "zerocost-privacy-settings-btn";
1942
+ btn.setAttribute("aria-label", "Privacy Settings");
1943
+ btn.title = "Privacy Settings";
1944
+ const pos = this.consentConfig.buttonPosition || "bottom-left";
1945
+ btn.className = `zc-settings-btn zc-settings-${pos}`;
1946
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
1947
+ btn.addEventListener("click", () => this.open());
1948
+ document.body.appendChild(btn);
1949
+ }
1950
+ removeSettingsButton() {
1951
+ document.getElementById("zerocost-privacy-settings-btn")?.remove();
1952
+ }
1877
1953
  };
1878
1954
 
1879
1955
  // src/index.ts
@@ -1907,21 +1983,15 @@ var ZerocostSDK = class {
1907
1983
  });
1908
1984
  }
1909
1985
  async init() {
1910
- this.core.init();
1911
1986
  if (typeof document === "undefined") {
1912
- this.core.log("Running in non-browser environment; skipping DOM injection.");
1913
1987
  return;
1914
1988
  }
1915
1989
  if (window !== window.top) {
1916
- this.core.log("Running inside an iframe. Ads render if permissions allow.");
1917
1990
  }
1918
- this.core.log("Initializing Zerocost SDK.");
1919
1991
  if (this.consent.shouldPrompt()) {
1920
- this.core.log("Consent required \u2014 showing prompt.");
1921
1992
  await this.consent.promptAndWait();
1922
1993
  }
1923
1994
  if (!this.consent.has("ads")) {
1924
- this.core.log("Ads consent not granted \u2014 skipping ad injection.");
1925
1995
  return;
1926
1996
  }
1927
1997
  const cachedConfig = this.readCachedConfig();
@@ -1929,12 +1999,11 @@ var ZerocostSDK = class {
1929
1999
  this.lastConfigHash = this.configToHash(cachedConfig);
1930
2000
  this.syncDataCollection(cachedConfig.dataCollection);
1931
2001
  await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
1932
- this.core.log("Applied cached config immediately.");
1933
2002
  }
1934
2003
  this.startConfigSync();
1935
2004
  try {
1936
2005
  await this.refreshConfig({ force: true, reason: "init" });
1937
- this.core.log("SDK fully initialized. Ads are rendering automatically.");
2006
+ this.core.log(`Zerocost SDK initialized (${this.core.getConfig().appId})`);
1938
2007
  } catch (err) {
1939
2008
  this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
1940
2009
  await this.widget.autoInject();
@@ -2108,6 +2177,10 @@ var ZerocostSDK = class {
2108
2177
  return { valid: false, error: err.message };
2109
2178
  }
2110
2179
  }
2180
+ /** Open the consent/privacy settings popup. */
2181
+ async showSettings() {
2182
+ return this.consent.open();
2183
+ }
2111
2184
  };
2112
2185
  export {
2113
2186
  ConsentManager,
@@ -11,6 +11,8 @@ export interface ZerocostConsentRecord {
11
11
  }
12
12
  export interface ConsentConfig {
13
13
  privacyPolicyUrl?: string;
14
+ showSettingsButton?: boolean;
15
+ buttonPosition?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
14
16
  onConsentChange?: (preferences: ZerocostConsentRecord) => void;
15
17
  }
16
18
  export type ConsentFeature = 'ads' | 'usageData' | 'aiInteractions';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerocost/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",