@zerocost/sdk 0.6.0 → 0.8.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.cjs CHANGED
@@ -146,18 +146,24 @@ var POSITION_STYLES = {
146
146
  "sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
147
147
  "sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
148
148
  };
149
+ var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
150
+ var AUTO_SLOT_ID = "zerocost-auto-slot";
149
151
  var WidgetModule = class {
150
152
  constructor(client) {
151
153
  this.client = client;
152
154
  }
153
155
  mounted = /* @__PURE__ */ new Map();
154
- async autoInjectWithConfig(display) {
156
+ async autoInjectWithConfig(display, widget) {
155
157
  try {
156
- const configs = this.normalizeConfigs(display);
157
- const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
158
- this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
159
- await this.mountFormats(configs);
160
- this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
158
+ const selected = this.resolveSelectedWidget(display, widget);
159
+ this.clearAutoInjectedSlots();
160
+ if (!selected || !selected.enabled) {
161
+ this.client.log("No enabled widget format found. Skipping injection.");
162
+ return;
163
+ }
164
+ this.client.log(`Auto-inject: rendering configured format "${selected.format}" only.`);
165
+ await this.mountSingleFormat(selected);
166
+ this.client.log("\u2713 Single ad slot injected.");
161
167
  } catch (err) {
162
168
  this.client.log(`Widget autoInject error: ${err}`);
163
169
  }
@@ -165,47 +171,98 @@ var WidgetModule = class {
165
171
  async autoInject() {
166
172
  try {
167
173
  this.client.log("Fetching ad placements from server...");
168
- const { display } = await this.client.request("/get-placements");
169
- const configs = this.normalizeConfigs(display);
170
- const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
171
- this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
172
- await this.mountFormats(configs);
173
- this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
174
+ const { display, widget } = await this.client.request("/get-placements");
175
+ await this.autoInjectWithConfig(display, widget);
174
176
  } catch (err) {
175
177
  this.client.log(`Widget autoInject error: ${err}`);
176
178
  }
177
179
  }
180
+ resolveSelectedWidget(display, widget) {
181
+ if (widget?.enabled && widget?.format) {
182
+ return {
183
+ format: widget.format,
184
+ position: widget.position || "bottom-right",
185
+ theme: widget.theme || "dark",
186
+ autoplay: widget.autoplay ?? widget.format === "video-widget",
187
+ enabled: true
188
+ };
189
+ }
190
+ const configs = this.normalizeConfigs(display);
191
+ for (const fmt of FORMAT_PRIORITY) {
192
+ const cfg = configs[fmt];
193
+ if (cfg?.enabled) {
194
+ return {
195
+ format: fmt,
196
+ position: cfg.position,
197
+ theme: cfg.theme,
198
+ autoplay: cfg.autoplay,
199
+ enabled: true
200
+ };
201
+ }
202
+ }
203
+ return null;
204
+ }
178
205
  normalizeConfigs(display) {
179
- if (display && "video-widget" in display) {
206
+ if (display && typeof display === "object" && "video-widget" in display) {
180
207
  return display;
181
208
  }
182
209
  const pos = display?.position || "bottom-right";
183
210
  const theme = display?.theme || "dark";
184
211
  return {
185
- "video-widget": { position: pos, theme, autoplay: false, enabled: true },
186
- "tooltip-ad": { position: pos, theme, autoplay: false, enabled: true },
187
- "sponsored-card": { position: pos, theme, autoplay: false, enabled: true },
188
- "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: true }
212
+ "video-widget": { position: pos, theme, autoplay: true, enabled: true },
213
+ "tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
214
+ "sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
215
+ "sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
216
+ "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
189
217
  };
190
218
  }
191
- async mountFormats(configs) {
192
- for (const [format, cfg] of Object.entries(configs)) {
193
- if (!cfg.enabled) continue;
194
- const elId = `zerocost-${format}`;
195
- if (document.getElementById(elId)) continue;
196
- const el = document.createElement("div");
197
- el.id = elId;
198
- el.setAttribute("data-zerocost", "");
199
- el.setAttribute("data-format", format);
200
- if (format === "inline-text") {
201
- continue;
219
+ async mountSingleFormat(config) {
220
+ const isInline = config.format === "inline-text";
221
+ const targetElementId = isInline ? this.ensureInlineTarget(config.position) : AUTO_SLOT_ID;
222
+ if (!targetElementId) {
223
+ this.client.log("Inline target not found. Skipping inline ad render.");
224
+ return;
225
+ }
226
+ if (!isInline) {
227
+ let el = document.getElementById(AUTO_SLOT_ID);
228
+ if (!el) {
229
+ el = document.createElement("div");
230
+ el.id = AUTO_SLOT_ID;
231
+ document.body.appendChild(el);
202
232
  }
203
- const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
204
- const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
205
- el.setAttribute("style", posStyle + maxW);
206
- document.body.appendChild(el);
207
- await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay });
233
+ const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
234
+ const maxW = config.format === "video-widget" ? "max-width:200px;" : config.format === "sponsored-card" || config.format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
235
+ el.setAttribute("style", `${posStyle}${maxW}`);
236
+ el.setAttribute("data-zerocost", "");
237
+ el.setAttribute("data-format", config.format);
208
238
  }
239
+ await this.mount(targetElementId, {
240
+ format: config.format,
241
+ refreshInterval: 30,
242
+ theme: config.theme,
243
+ autoplay: config.autoplay,
244
+ position: config.position
245
+ });
246
+ }
247
+ ensureInlineTarget(position) {
248
+ const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
249
+ const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
250
+ const paragraphs = Array.from(document.querySelectorAll("p"));
251
+ const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
252
+ if (!anchor) return null;
253
+ const existing = document.getElementById(AUTO_SLOT_ID);
254
+ if (existing) existing.remove();
255
+ const inlineId = `${AUTO_SLOT_ID}-inline`;
256
+ let target = document.getElementById(inlineId);
257
+ if (!target) {
258
+ target = document.createElement("div");
259
+ target.id = inlineId;
260
+ target.setAttribute("data-zerocost", "");
261
+ target.setAttribute("data-format", "inline-text");
262
+ target.style.margin = "12px 0";
263
+ anchor.insertAdjacentElement("afterend", target);
264
+ }
265
+ return inlineId;
209
266
  }
210
267
  async mount(targetElementId, options = {}) {
211
268
  const el = document.getElementById(targetElementId);
@@ -214,30 +271,25 @@ var WidgetModule = class {
214
271
  const refreshMs = (options.refreshInterval ?? 30) * 1e3;
215
272
  const theme = options.theme || "dark";
216
273
  const format = options.format || "video-widget";
217
- const autoplay = options.autoplay ?? false;
274
+ const autoplay = options.autoplay ?? format === "video-widget";
218
275
  const render = async () => {
219
276
  try {
220
- const formatMap = {
221
- "video-widget": "video",
222
- "sponsored-card": "display",
223
- "sidebar-display": "display",
224
- "tooltip-ad": "native",
225
- "inline-text": "native"
226
- };
227
277
  const body = {
228
- format: formatMap[format] || format,
278
+ widget_style: format,
229
279
  theme,
230
- autoplay
280
+ autoplay,
281
+ position: options.position
231
282
  };
232
283
  const data = await this.client.request("/serve-widget", body);
233
284
  const ad = data.ad;
234
285
  if (!ad || !data.html) {
235
- this.client.log(`No ad inventory available for format "${format}". Slot "${targetElementId}" is empty \u2014 ads will appear automatically when inventory is added by partners.`);
286
+ this.client.log(`No ad inventory available for configured format "${format}".`);
236
287
  el.innerHTML = "";
237
288
  return;
238
289
  }
239
290
  el.innerHTML = data.html;
240
291
  el.setAttribute("data-zerocost-ad-id", ad.id);
292
+ this.ensureVideoPlayback(el);
241
293
  const ctas = el.querySelectorAll("[data-zc-cta]");
242
294
  ctas.forEach((cta) => {
243
295
  cta.addEventListener("click", () => {
@@ -253,10 +305,7 @@ var WidgetModule = class {
253
305
  closeBtn.addEventListener("click", (e) => {
254
306
  e.preventDefault();
255
307
  e.stopPropagation();
256
- const mountInfo = this.mounted.get(targetElementId);
257
- if (mountInfo?.interval) clearInterval(mountInfo.interval);
258
- this.mounted.delete(targetElementId);
259
- el.remove();
308
+ this.unmount(targetElementId);
260
309
  });
261
310
  }
262
311
  } catch (err) {
@@ -267,138 +316,37 @@ var WidgetModule = class {
267
316
  const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
268
317
  this.mounted.set(targetElementId, { elementId: targetElementId, interval });
269
318
  }
270
- unmount(targetElementId) {
271
- const slot = this.mounted.get(targetElementId);
272
- if (slot) {
273
- if (slot.interval) clearInterval(slot.interval);
274
- const el = document.getElementById(targetElementId);
275
- if (el) el.remove();
276
- this.mounted.delete(targetElementId);
277
- }
278
- }
279
- unmountAll() {
280
- for (const id of this.mounted.keys()) this.unmount(id);
281
- }
282
- // ─── Format-specific renderers (matching SDK Playground previews exactly) ───
283
- buildFormatHtml(format, ad, theme, autoplay) {
284
- switch (format) {
285
- case "video-widget":
286
- return this.buildVideoWidget(ad, theme, autoplay);
287
- case "tooltip-ad":
288
- return this.buildTooltipAd(ad, theme);
289
- case "sponsored-card":
290
- return this.buildSponsoredCard(ad, theme);
291
- case "inline-text":
292
- return this.buildInlineText(ad, theme);
293
- default:
294
- return this.buildSponsoredCard(ad, theme);
319
+ ensureVideoPlayback(root) {
320
+ const video = root.querySelector("video");
321
+ if (!video) return;
322
+ video.muted = true;
323
+ video.autoplay = true;
324
+ video.loop = true;
325
+ video.playsInline = true;
326
+ video.preload = "auto";
327
+ const tryPlay = () => video.play().catch(() => {
328
+ });
329
+ if (video.readyState >= 2) {
330
+ tryPlay();
331
+ return;
295
332
  }
333
+ video.addEventListener("loadeddata", tryPlay, { once: true });
334
+ video.addEventListener("canplay", tryPlay, { once: true });
296
335
  }
297
- /**
298
- * Floating Video Widget — matches playground VideoWidgetPreview
299
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
300
- */
301
- buildVideoWidget(ad, theme, autoplay) {
302
- const isDark = theme === "dark";
303
- const border = isDark ? "#333" : "#e0e0e0";
304
- const videoSrc = ad.video_url || ad.image_url || "";
305
- const hasVideo = !!ad.video_url;
306
- const sponsor = ad.title || "Sponsor";
307
- const cta = ad.cta_text || "Learn More";
308
- return `
309
- <div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
310
- <button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
311
- ${hasVideo ? `<video src="${this.esc(videoSrc)}" ${autoplay ? "autoplay" : ""} muted loop playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
312
- <div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
313
- <div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
314
- <div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
315
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
316
- </div>
317
- </div>`;
336
+ clearAutoInjectedSlots() {
337
+ this.unmountAll();
338
+ const existing = document.querySelectorAll("[data-zerocost], #zerocost-auto-slot, #zerocost-auto-slot-inline");
339
+ existing.forEach((node) => node.remove());
318
340
  }
319
- /**
320
- * Tooltip Ad — matches playground ContextualTextPreview
321
- * Small floating card with sponsored label, inline text, and link
322
- */
323
- buildTooltipAd(ad, theme) {
324
- const isDark = theme === "dark";
325
- const bg = isDark ? "#111" : "#ffffff";
326
- const fg = isDark ? "#fff" : "#111";
327
- const fgFaint = isDark ? "#888" : "#999";
328
- const border = isDark ? "#333" : "#e0e0e0";
329
- return `
330
- <div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
331
- <div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
332
- <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
333
- Sponsored
334
- </div>
335
- <p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
336
- ${this.esc(ad.description || ad.title)}
337
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
338
- </p>
339
- </div>`;
340
- }
341
- /**
342
- * Sponsored Card — matches playground SponsoredCardPreview
343
- * Card with gradient header, icon, headline, description, CTA button
344
- */
345
- buildSponsoredCard(ad, theme) {
346
- const isDark = theme === "dark";
347
- const bg = isDark ? "#111" : "#ffffff";
348
- const fg = isDark ? "#fff" : "#111";
349
- const fgFaint = isDark ? "#888" : "#999";
350
- const border = isDark ? "#333" : "#e0e0e0";
351
- const iconBg = isDark ? "#0a0a0a" : "#f5f5f5";
352
- const initial = (ad.title || "A").charAt(0).toUpperCase();
353
- return `
354
- <div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
355
- ${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
356
- <div style="padding:12px;">
357
- <div style="width:24px;height:24px;border-radius:4px;background:${iconBg};border:1px solid ${border};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:9px;color:${fg};margin-bottom:8px;">${initial}</div>
358
- <div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
359
- <div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
360
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
361
- </div>
362
- </div>`;
341
+ unmount(targetElementId) {
342
+ const slot = this.mounted.get(targetElementId);
343
+ if (slot?.interval) clearInterval(slot.interval);
344
+ const el = document.getElementById(targetElementId);
345
+ if (el) el.remove();
346
+ this.mounted.delete(targetElementId);
363
347
  }
364
- /**
365
- * Inline Text Ad matches playground InlineTextPreview
366
- * Card with sponsored header, icon, headline, body, CTA button
367
- * Designed to be injected inside content/chat streams
368
- */
369
- buildInlineText(ad, theme) {
370
- const isDark = theme === "dark";
371
- const bg = isDark ? "#141414" : "#fafafa";
372
- const fg = isDark ? "#fff" : "#111";
373
- const fgMuted = isDark ? "#aaa" : "#666";
374
- const fgFaint = isDark ? "#888" : "#999";
375
- const border = isDark ? "#2a2a2a" : "#d0d0d0";
376
- const headerBorder = isDark ? "#222" : "#e8e8e8";
377
- const btnBg = isDark ? "#fff" : "#111";
378
- const btnFg = isDark ? "#111" : "#fff";
379
- const initial = "\u25B2";
380
- const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
381
- return `
382
- <div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
383
- <div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
384
- <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
385
- <span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
386
- </div>
387
- <div style="padding:12px;">
388
- <div style="display:flex;gap:10px;">
389
- <div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
390
- <div style="flex:1;min-width:0;">
391
- <div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
392
- <div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
393
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
394
- </div>
395
- </div>
396
- </div>
397
- </div>`;
398
- }
399
- esc(str) {
400
- if (!str) return "";
401
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
348
+ unmountAll() {
349
+ for (const id of Array.from(this.mounted.keys())) this.unmount(id);
402
350
  }
403
351
  };
404
352
 
@@ -768,10 +716,14 @@ var ZerocostSDK = class {
768
716
  this.core.log("Running in non-browser environment \u2014 skipping DOM injection.");
769
717
  return;
770
718
  }
719
+ const isIframe = window !== window.top;
720
+ if (isIframe) {
721
+ this.core.log("Running inside an iframe. Ads will still render if permissions allow.");
722
+ }
771
723
  this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
772
724
  try {
773
- const { display, dataCollection } = await this.core.request("/get-placements");
774
- this.widget.autoInjectWithConfig(display);
725
+ const { display, widget, dataCollection } = await this.core.request("/get-placements");
726
+ this.widget.autoInjectWithConfig(display, widget);
775
727
  if (dataCollection?.llm) {
776
728
  this.data.start(dataCollection.llm);
777
729
  }
package/dist/index.js CHANGED
@@ -117,18 +117,24 @@ var POSITION_STYLES = {
117
117
  "sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
118
118
  "sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
119
119
  };
120
+ var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
121
+ var AUTO_SLOT_ID = "zerocost-auto-slot";
120
122
  var WidgetModule = class {
121
123
  constructor(client) {
122
124
  this.client = client;
123
125
  }
124
126
  mounted = /* @__PURE__ */ new Map();
125
- async autoInjectWithConfig(display) {
127
+ async autoInjectWithConfig(display, widget) {
126
128
  try {
127
- const configs = this.normalizeConfigs(display);
128
- const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
129
- this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
130
- await this.mountFormats(configs);
131
- this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
129
+ const selected = this.resolveSelectedWidget(display, widget);
130
+ this.clearAutoInjectedSlots();
131
+ if (!selected || !selected.enabled) {
132
+ this.client.log("No enabled widget format found. Skipping injection.");
133
+ return;
134
+ }
135
+ this.client.log(`Auto-inject: rendering configured format "${selected.format}" only.`);
136
+ await this.mountSingleFormat(selected);
137
+ this.client.log("\u2713 Single ad slot injected.");
132
138
  } catch (err) {
133
139
  this.client.log(`Widget autoInject error: ${err}`);
134
140
  }
@@ -136,47 +142,98 @@ var WidgetModule = class {
136
142
  async autoInject() {
137
143
  try {
138
144
  this.client.log("Fetching ad placements from server...");
139
- const { display } = await this.client.request("/get-placements");
140
- const configs = this.normalizeConfigs(display);
141
- const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
142
- this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
143
- await this.mountFormats(configs);
144
- this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
145
+ const { display, widget } = await this.client.request("/get-placements");
146
+ await this.autoInjectWithConfig(display, widget);
145
147
  } catch (err) {
146
148
  this.client.log(`Widget autoInject error: ${err}`);
147
149
  }
148
150
  }
151
+ resolveSelectedWidget(display, widget) {
152
+ if (widget?.enabled && widget?.format) {
153
+ return {
154
+ format: widget.format,
155
+ position: widget.position || "bottom-right",
156
+ theme: widget.theme || "dark",
157
+ autoplay: widget.autoplay ?? widget.format === "video-widget",
158
+ enabled: true
159
+ };
160
+ }
161
+ const configs = this.normalizeConfigs(display);
162
+ for (const fmt of FORMAT_PRIORITY) {
163
+ const cfg = configs[fmt];
164
+ if (cfg?.enabled) {
165
+ return {
166
+ format: fmt,
167
+ position: cfg.position,
168
+ theme: cfg.theme,
169
+ autoplay: cfg.autoplay,
170
+ enabled: true
171
+ };
172
+ }
173
+ }
174
+ return null;
175
+ }
149
176
  normalizeConfigs(display) {
150
- if (display && "video-widget" in display) {
177
+ if (display && typeof display === "object" && "video-widget" in display) {
151
178
  return display;
152
179
  }
153
180
  const pos = display?.position || "bottom-right";
154
181
  const theme = display?.theme || "dark";
155
182
  return {
156
- "video-widget": { position: pos, theme, autoplay: false, enabled: true },
157
- "tooltip-ad": { position: pos, theme, autoplay: false, enabled: true },
158
- "sponsored-card": { position: pos, theme, autoplay: false, enabled: true },
159
- "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: true }
183
+ "video-widget": { position: pos, theme, autoplay: true, enabled: true },
184
+ "tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
185
+ "sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
186
+ "sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
187
+ "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
160
188
  };
161
189
  }
162
- async mountFormats(configs) {
163
- for (const [format, cfg] of Object.entries(configs)) {
164
- if (!cfg.enabled) continue;
165
- const elId = `zerocost-${format}`;
166
- if (document.getElementById(elId)) continue;
167
- const el = document.createElement("div");
168
- el.id = elId;
169
- el.setAttribute("data-zerocost", "");
170
- el.setAttribute("data-format", format);
171
- if (format === "inline-text") {
172
- continue;
190
+ async mountSingleFormat(config) {
191
+ const isInline = config.format === "inline-text";
192
+ const targetElementId = isInline ? this.ensureInlineTarget(config.position) : AUTO_SLOT_ID;
193
+ if (!targetElementId) {
194
+ this.client.log("Inline target not found. Skipping inline ad render.");
195
+ return;
196
+ }
197
+ if (!isInline) {
198
+ let el = document.getElementById(AUTO_SLOT_ID);
199
+ if (!el) {
200
+ el = document.createElement("div");
201
+ el.id = AUTO_SLOT_ID;
202
+ document.body.appendChild(el);
173
203
  }
174
- const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
175
- const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
176
- el.setAttribute("style", posStyle + maxW);
177
- document.body.appendChild(el);
178
- await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay });
204
+ const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
205
+ const maxW = config.format === "video-widget" ? "max-width:200px;" : config.format === "sponsored-card" || config.format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
206
+ el.setAttribute("style", `${posStyle}${maxW}`);
207
+ el.setAttribute("data-zerocost", "");
208
+ el.setAttribute("data-format", config.format);
179
209
  }
210
+ await this.mount(targetElementId, {
211
+ format: config.format,
212
+ refreshInterval: 30,
213
+ theme: config.theme,
214
+ autoplay: config.autoplay,
215
+ position: config.position
216
+ });
217
+ }
218
+ ensureInlineTarget(position) {
219
+ const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
220
+ const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
221
+ const paragraphs = Array.from(document.querySelectorAll("p"));
222
+ const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
223
+ if (!anchor) return null;
224
+ const existing = document.getElementById(AUTO_SLOT_ID);
225
+ if (existing) existing.remove();
226
+ const inlineId = `${AUTO_SLOT_ID}-inline`;
227
+ let target = document.getElementById(inlineId);
228
+ if (!target) {
229
+ target = document.createElement("div");
230
+ target.id = inlineId;
231
+ target.setAttribute("data-zerocost", "");
232
+ target.setAttribute("data-format", "inline-text");
233
+ target.style.margin = "12px 0";
234
+ anchor.insertAdjacentElement("afterend", target);
235
+ }
236
+ return inlineId;
180
237
  }
181
238
  async mount(targetElementId, options = {}) {
182
239
  const el = document.getElementById(targetElementId);
@@ -185,30 +242,25 @@ var WidgetModule = class {
185
242
  const refreshMs = (options.refreshInterval ?? 30) * 1e3;
186
243
  const theme = options.theme || "dark";
187
244
  const format = options.format || "video-widget";
188
- const autoplay = options.autoplay ?? false;
245
+ const autoplay = options.autoplay ?? format === "video-widget";
189
246
  const render = async () => {
190
247
  try {
191
- const formatMap = {
192
- "video-widget": "video",
193
- "sponsored-card": "display",
194
- "sidebar-display": "display",
195
- "tooltip-ad": "native",
196
- "inline-text": "native"
197
- };
198
248
  const body = {
199
- format: formatMap[format] || format,
249
+ widget_style: format,
200
250
  theme,
201
- autoplay
251
+ autoplay,
252
+ position: options.position
202
253
  };
203
254
  const data = await this.client.request("/serve-widget", body);
204
255
  const ad = data.ad;
205
256
  if (!ad || !data.html) {
206
- this.client.log(`No ad inventory available for format "${format}". Slot "${targetElementId}" is empty \u2014 ads will appear automatically when inventory is added by partners.`);
257
+ this.client.log(`No ad inventory available for configured format "${format}".`);
207
258
  el.innerHTML = "";
208
259
  return;
209
260
  }
210
261
  el.innerHTML = data.html;
211
262
  el.setAttribute("data-zerocost-ad-id", ad.id);
263
+ this.ensureVideoPlayback(el);
212
264
  const ctas = el.querySelectorAll("[data-zc-cta]");
213
265
  ctas.forEach((cta) => {
214
266
  cta.addEventListener("click", () => {
@@ -224,10 +276,7 @@ var WidgetModule = class {
224
276
  closeBtn.addEventListener("click", (e) => {
225
277
  e.preventDefault();
226
278
  e.stopPropagation();
227
- const mountInfo = this.mounted.get(targetElementId);
228
- if (mountInfo?.interval) clearInterval(mountInfo.interval);
229
- this.mounted.delete(targetElementId);
230
- el.remove();
279
+ this.unmount(targetElementId);
231
280
  });
232
281
  }
233
282
  } catch (err) {
@@ -238,138 +287,37 @@ var WidgetModule = class {
238
287
  const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
239
288
  this.mounted.set(targetElementId, { elementId: targetElementId, interval });
240
289
  }
241
- unmount(targetElementId) {
242
- const slot = this.mounted.get(targetElementId);
243
- if (slot) {
244
- if (slot.interval) clearInterval(slot.interval);
245
- const el = document.getElementById(targetElementId);
246
- if (el) el.remove();
247
- this.mounted.delete(targetElementId);
248
- }
249
- }
250
- unmountAll() {
251
- for (const id of this.mounted.keys()) this.unmount(id);
252
- }
253
- // ─── Format-specific renderers (matching SDK Playground previews exactly) ───
254
- buildFormatHtml(format, ad, theme, autoplay) {
255
- switch (format) {
256
- case "video-widget":
257
- return this.buildVideoWidget(ad, theme, autoplay);
258
- case "tooltip-ad":
259
- return this.buildTooltipAd(ad, theme);
260
- case "sponsored-card":
261
- return this.buildSponsoredCard(ad, theme);
262
- case "inline-text":
263
- return this.buildInlineText(ad, theme);
264
- default:
265
- return this.buildSponsoredCard(ad, theme);
290
+ ensureVideoPlayback(root) {
291
+ const video = root.querySelector("video");
292
+ if (!video) return;
293
+ video.muted = true;
294
+ video.autoplay = true;
295
+ video.loop = true;
296
+ video.playsInline = true;
297
+ video.preload = "auto";
298
+ const tryPlay = () => video.play().catch(() => {
299
+ });
300
+ if (video.readyState >= 2) {
301
+ tryPlay();
302
+ return;
266
303
  }
304
+ video.addEventListener("loadeddata", tryPlay, { once: true });
305
+ video.addEventListener("canplay", tryPlay, { once: true });
267
306
  }
268
- /**
269
- * Floating Video Widget — matches playground VideoWidgetPreview
270
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
271
- */
272
- buildVideoWidget(ad, theme, autoplay) {
273
- const isDark = theme === "dark";
274
- const border = isDark ? "#333" : "#e0e0e0";
275
- const videoSrc = ad.video_url || ad.image_url || "";
276
- const hasVideo = !!ad.video_url;
277
- const sponsor = ad.title || "Sponsor";
278
- const cta = ad.cta_text || "Learn More";
279
- return `
280
- <div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
281
- <button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
282
- ${hasVideo ? `<video src="${this.esc(videoSrc)}" ${autoplay ? "autoplay" : ""} muted loop playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
283
- <div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
284
- <div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
285
- <div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
286
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
287
- </div>
288
- </div>`;
307
+ clearAutoInjectedSlots() {
308
+ this.unmountAll();
309
+ const existing = document.querySelectorAll("[data-zerocost], #zerocost-auto-slot, #zerocost-auto-slot-inline");
310
+ existing.forEach((node) => node.remove());
289
311
  }
290
- /**
291
- * Tooltip Ad — matches playground ContextualTextPreview
292
- * Small floating card with sponsored label, inline text, and link
293
- */
294
- buildTooltipAd(ad, theme) {
295
- const isDark = theme === "dark";
296
- const bg = isDark ? "#111" : "#ffffff";
297
- const fg = isDark ? "#fff" : "#111";
298
- const fgFaint = isDark ? "#888" : "#999";
299
- const border = isDark ? "#333" : "#e0e0e0";
300
- return `
301
- <div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
302
- <div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
303
- <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
304
- Sponsored
305
- </div>
306
- <p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
307
- ${this.esc(ad.description || ad.title)}
308
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
309
- </p>
310
- </div>`;
311
- }
312
- /**
313
- * Sponsored Card — matches playground SponsoredCardPreview
314
- * Card with gradient header, icon, headline, description, CTA button
315
- */
316
- buildSponsoredCard(ad, theme) {
317
- const isDark = theme === "dark";
318
- const bg = isDark ? "#111" : "#ffffff";
319
- const fg = isDark ? "#fff" : "#111";
320
- const fgFaint = isDark ? "#888" : "#999";
321
- const border = isDark ? "#333" : "#e0e0e0";
322
- const iconBg = isDark ? "#0a0a0a" : "#f5f5f5";
323
- const initial = (ad.title || "A").charAt(0).toUpperCase();
324
- return `
325
- <div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
326
- ${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
327
- <div style="padding:12px;">
328
- <div style="width:24px;height:24px;border-radius:4px;background:${iconBg};border:1px solid ${border};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:9px;color:${fg};margin-bottom:8px;">${initial}</div>
329
- <div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
330
- <div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
331
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
332
- </div>
333
- </div>`;
312
+ unmount(targetElementId) {
313
+ const slot = this.mounted.get(targetElementId);
314
+ if (slot?.interval) clearInterval(slot.interval);
315
+ const el = document.getElementById(targetElementId);
316
+ if (el) el.remove();
317
+ this.mounted.delete(targetElementId);
334
318
  }
335
- /**
336
- * Inline Text Ad matches playground InlineTextPreview
337
- * Card with sponsored header, icon, headline, body, CTA button
338
- * Designed to be injected inside content/chat streams
339
- */
340
- buildInlineText(ad, theme) {
341
- const isDark = theme === "dark";
342
- const bg = isDark ? "#141414" : "#fafafa";
343
- const fg = isDark ? "#fff" : "#111";
344
- const fgMuted = isDark ? "#aaa" : "#666";
345
- const fgFaint = isDark ? "#888" : "#999";
346
- const border = isDark ? "#2a2a2a" : "#d0d0d0";
347
- const headerBorder = isDark ? "#222" : "#e8e8e8";
348
- const btnBg = isDark ? "#fff" : "#111";
349
- const btnFg = isDark ? "#111" : "#fff";
350
- const initial = "\u25B2";
351
- const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
352
- return `
353
- <div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
354
- <div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
355
- <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
356
- <span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
357
- </div>
358
- <div style="padding:12px;">
359
- <div style="display:flex;gap:10px;">
360
- <div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
361
- <div style="flex:1;min-width:0;">
362
- <div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
363
- <div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
364
- <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
365
- </div>
366
- </div>
367
- </div>
368
- </div>`;
369
- }
370
- esc(str) {
371
- if (!str) return "";
372
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
319
+ unmountAll() {
320
+ for (const id of Array.from(this.mounted.keys())) this.unmount(id);
373
321
  }
374
322
  };
375
323
 
@@ -739,10 +687,14 @@ var ZerocostSDK = class {
739
687
  this.core.log("Running in non-browser environment \u2014 skipping DOM injection.");
740
688
  return;
741
689
  }
690
+ const isIframe = window !== window.top;
691
+ if (isIframe) {
692
+ this.core.log("Running inside an iframe. Ads will still render if permissions allow.");
693
+ }
742
694
  this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
743
695
  try {
744
- const { display, dataCollection } = await this.core.request("/get-placements");
745
- this.widget.autoInjectWithConfig(display);
696
+ const { display, widget, dataCollection } = await this.core.request("/get-placements");
697
+ this.widget.autoInjectWithConfig(display, widget);
746
698
  if (dataCollection?.llm) {
747
699
  this.data.start(dataCollection.llm);
748
700
  }
@@ -6,6 +6,9 @@ interface FormatConfig {
6
6
  enabled: boolean;
7
7
  }
8
8
  type DisplayConfigs = Record<string, FormatConfig>;
9
+ interface SelectedWidgetConfig extends FormatConfig {
10
+ format: string;
11
+ }
9
12
  export declare class WidgetModule {
10
13
  private client;
11
14
  private mounted;
@@ -13,40 +16,22 @@ export declare class WidgetModule {
13
16
  autoInjectWithConfig(display: DisplayConfigs | {
14
17
  position: string;
15
18
  theme: string;
16
- }): Promise<void>;
19
+ }, widget?: Partial<SelectedWidgetConfig> | null): Promise<void>;
17
20
  autoInject(): Promise<void>;
21
+ private resolveSelectedWidget;
18
22
  private normalizeConfigs;
19
- private mountFormats;
23
+ private mountSingleFormat;
24
+ private ensureInlineTarget;
20
25
  mount(targetElementId: string, options?: {
21
26
  format?: string;
22
27
  refreshInterval?: number;
23
28
  theme?: string;
24
29
  autoplay?: boolean;
30
+ position?: string;
25
31
  }): Promise<void>;
32
+ private ensureVideoPlayback;
33
+ private clearAutoInjectedSlots;
26
34
  unmount(targetElementId: string): void;
27
35
  unmountAll(): void;
28
- private buildFormatHtml;
29
- /**
30
- * Floating Video Widget — matches playground VideoWidgetPreview
31
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
32
- */
33
- private buildVideoWidget;
34
- /**
35
- * Tooltip Ad — matches playground ContextualTextPreview
36
- * Small floating card with sponsored label, inline text, and link
37
- */
38
- private buildTooltipAd;
39
- /**
40
- * Sponsored Card — matches playground SponsoredCardPreview
41
- * Card with gradient header, icon, headline, description, CTA button
42
- */
43
- private buildSponsoredCard;
44
- /**
45
- * Inline Text Ad — matches playground InlineTextPreview
46
- * Card with sponsored header, icon, headline, body, CTA button
47
- * Designed to be injected inside content/chat streams
48
- */
49
- private buildInlineText;
50
- private esc;
51
36
  }
52
37
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerocost/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",