@zerocost/sdk 0.7.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,23 +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", "sponsored-card", "sidebar-display", "tooltip-ad", "inline-text"];
149
+ var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
150
+ var AUTO_SLOT_ID = "zerocost-auto-slot";
150
151
  var WidgetModule = class {
151
152
  constructor(client) {
152
153
  this.client = client;
153
154
  }
154
155
  mounted = /* @__PURE__ */ new Map();
155
- async autoInjectWithConfig(display) {
156
+ async autoInjectWithConfig(display, widget) {
156
157
  try {
157
- const configs = this.normalizeConfigs(display);
158
- const chosen = this.pickOneFormat(configs);
159
- if (!chosen) {
160
- this.client.log("No ad formats enabled. Skipping injection.");
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.");
161
162
  return;
162
163
  }
163
- this.client.log(`Auto-inject: rendering "${chosen.format}" only.`);
164
- await this.mountSingleFormat(chosen.format, chosen.config);
165
- this.client.log("\u2713 Ad slot injected.");
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.");
166
167
  } catch (err) {
167
168
  this.client.log(`Widget autoInject error: ${err}`);
168
169
  }
@@ -170,21 +171,39 @@ var WidgetModule = class {
170
171
  async autoInject() {
171
172
  try {
172
173
  this.client.log("Fetching ad placements from server...");
173
- const { display } = await this.client.request("/get-placements");
174
- await this.autoInjectWithConfig(display);
174
+ const { display, widget } = await this.client.request("/get-placements");
175
+ await this.autoInjectWithConfig(display, widget);
175
176
  } catch (err) {
176
177
  this.client.log(`Widget autoInject error: ${err}`);
177
178
  }
178
179
  }
179
- pickOneFormat(configs) {
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);
180
191
  for (const fmt of FORMAT_PRIORITY) {
181
192
  const cfg = configs[fmt];
182
- if (cfg?.enabled) return { format: fmt, config: cfg };
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
+ }
183
202
  }
184
203
  return null;
185
204
  }
186
205
  normalizeConfigs(display) {
187
- if (display && "video-widget" in display) {
206
+ if (display && typeof display === "object" && "video-widget" in display) {
188
207
  return display;
189
208
  }
190
209
  const pos = display?.position || "bottom-right";
@@ -193,22 +212,57 @@ var WidgetModule = class {
193
212
  "video-widget": { position: pos, theme, autoplay: true, enabled: true },
194
213
  "tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
195
214
  "sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
215
+ "sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
196
216
  "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
197
217
  };
198
218
  }
199
- async mountSingleFormat(format, cfg) {
200
- const elId = `zerocost-${format}`;
201
- if (document.getElementById(elId)) return;
202
- if (format === "inline-text") return;
203
- const el = document.createElement("div");
204
- el.id = elId;
205
- el.setAttribute("data-zerocost", "");
206
- el.setAttribute("data-format", format);
207
- const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
208
- const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
209
- el.setAttribute("style", posStyle + maxW);
210
- document.body.appendChild(el);
211
- await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay ?? true });
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);
232
+ }
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);
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;
212
266
  }
213
267
  async mount(targetElementId, options = {}) {
214
268
  const el = document.getElementById(targetElementId);
@@ -217,36 +271,25 @@ var WidgetModule = class {
217
271
  const refreshMs = (options.refreshInterval ?? 30) * 1e3;
218
272
  const theme = options.theme || "dark";
219
273
  const format = options.format || "video-widget";
220
- const autoplay = options.autoplay ?? false;
274
+ const autoplay = options.autoplay ?? format === "video-widget";
221
275
  const render = async () => {
222
276
  try {
223
- const formatMap = {
224
- "video-widget": "video",
225
- "sponsored-card": "display",
226
- "sidebar-display": "display",
227
- "tooltip-ad": "native",
228
- "inline-text": "native"
229
- };
230
277
  const body = {
231
- format: formatMap[format] || format,
278
+ widget_style: format,
232
279
  theme,
233
- autoplay
280
+ autoplay,
281
+ position: options.position
234
282
  };
235
283
  const data = await this.client.request("/serve-widget", body);
236
284
  const ad = data.ad;
237
285
  if (!ad || !data.html) {
238
- 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}".`);
239
287
  el.innerHTML = "";
240
288
  return;
241
289
  }
242
290
  el.innerHTML = data.html;
243
291
  el.setAttribute("data-zerocost-ad-id", ad.id);
244
- const video = el.querySelector("video");
245
- if (video) {
246
- video.muted = true;
247
- video.play().catch(() => {
248
- });
249
- }
292
+ this.ensureVideoPlayback(el);
250
293
  const ctas = el.querySelectorAll("[data-zc-cta]");
251
294
  ctas.forEach((cta) => {
252
295
  cta.addEventListener("click", () => {
@@ -262,10 +305,7 @@ var WidgetModule = class {
262
305
  closeBtn.addEventListener("click", (e) => {
263
306
  e.preventDefault();
264
307
  e.stopPropagation();
265
- const mountInfo = this.mounted.get(targetElementId);
266
- if (mountInfo?.interval) clearInterval(mountInfo.interval);
267
- this.mounted.delete(targetElementId);
268
- el.remove();
308
+ this.unmount(targetElementId);
269
309
  });
270
310
  }
271
311
  } catch (err) {
@@ -276,138 +316,37 @@ var WidgetModule = class {
276
316
  const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
277
317
  this.mounted.set(targetElementId, { elementId: targetElementId, interval });
278
318
  }
279
- unmount(targetElementId) {
280
- const slot = this.mounted.get(targetElementId);
281
- if (slot) {
282
- if (slot.interval) clearInterval(slot.interval);
283
- const el = document.getElementById(targetElementId);
284
- if (el) el.remove();
285
- this.mounted.delete(targetElementId);
286
- }
287
- }
288
- unmountAll() {
289
- for (const id of this.mounted.keys()) this.unmount(id);
290
- }
291
- // ─── Format-specific renderers (matching SDK Playground previews exactly) ───
292
- buildFormatHtml(format, ad, theme, autoplay) {
293
- switch (format) {
294
- case "video-widget":
295
- return this.buildVideoWidget(ad, theme, autoplay);
296
- case "tooltip-ad":
297
- return this.buildTooltipAd(ad, theme);
298
- case "sponsored-card":
299
- return this.buildSponsoredCard(ad, theme);
300
- case "inline-text":
301
- return this.buildInlineText(ad, theme);
302
- default:
303
- 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;
304
332
  }
333
+ video.addEventListener("loadeddata", tryPlay, { once: true });
334
+ video.addEventListener("canplay", tryPlay, { once: true });
305
335
  }
306
- /**
307
- * Floating Video Widget — matches playground VideoWidgetPreview
308
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
309
- */
310
- buildVideoWidget(ad, theme, autoplay) {
311
- const isDark = theme === "dark";
312
- const border = isDark ? "#333" : "#e0e0e0";
313
- const videoSrc = ad.video_url || ad.image_url || "";
314
- const hasVideo = !!ad.video_url;
315
- const sponsor = ad.title || "Sponsor";
316
- const cta = ad.cta_text || "Learn More";
317
- return `
318
- <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"});">
319
- <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>
320
- ${hasVideo ? `<video src="${this.esc(videoSrc)}" autoplay muted loop playsinline preload="auto" 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;" />`}
321
- <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;">
322
- <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>
323
- <div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
324
- <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>
325
- </div>
326
- </div>`;
327
- }
328
- /**
329
- * Tooltip Ad — matches playground ContextualTextPreview
330
- * Small floating card with sponsored label, inline text, and link
331
- */
332
- buildTooltipAd(ad, theme) {
333
- const isDark = theme === "dark";
334
- const bg = isDark ? "#111" : "#ffffff";
335
- const fg = isDark ? "#fff" : "#111";
336
- const fgFaint = isDark ? "#888" : "#999";
337
- const border = isDark ? "#333" : "#e0e0e0";
338
- return `
339
- <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;position:relative;">
340
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
341
- <div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
342
- <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
343
- Sponsored
344
- </div>
345
- <p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
346
- ${this.esc(ad.description || ad.title)}
347
- <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>
348
- </p>
349
- </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());
350
340
  }
351
- /**
352
- * Sponsored Card — matches playground SponsoredCardPreview
353
- * Card with gradient header, icon, headline, description, CTA button
354
- */
355
- buildSponsoredCard(ad, theme) {
356
- const isDark = theme === "dark";
357
- const bg = isDark ? "#111" : "#ffffff";
358
- const fg = isDark ? "#fff" : "#111";
359
- const fgFaint = isDark ? "#888" : "#999";
360
- const border = isDark ? "#333" : "#e0e0e0";
361
- return `
362
- <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;position:relative;">
363
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;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:12px;line-height:1;">\u2715</button>
364
- ${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>`}
365
- <div style="padding:12px;">
366
- <div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
367
- <div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
368
- <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>
369
- </div>
370
- </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);
371
347
  }
372
- /**
373
- * Inline Text Ad matches playground InlineTextPreview
374
- * Card with sponsored header, icon, headline, body, CTA button
375
- * Designed to be injected inside content/chat streams
376
- */
377
- buildInlineText(ad, theme) {
378
- const isDark = theme === "dark";
379
- const bg = isDark ? "#141414" : "#fafafa";
380
- const fg = isDark ? "#fff" : "#111";
381
- const fgMuted = isDark ? "#aaa" : "#666";
382
- const fgFaint = isDark ? "#888" : "#999";
383
- const border = isDark ? "#2a2a2a" : "#d0d0d0";
384
- const headerBorder = isDark ? "#222" : "#e8e8e8";
385
- const btnBg = isDark ? "#fff" : "#111";
386
- const btnFg = isDark ? "#111" : "#fff";
387
- const initial = "\u25B2";
388
- const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
389
- return `
390
- <div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;position:relative;">
391
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
392
- <div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
393
- <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
394
- <span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
395
- </div>
396
- <div style="padding:12px;">
397
- <div style="display:flex;gap:10px;">
398
- <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>
399
- <div style="flex:1;min-width:0;">
400
- <div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
401
- <div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
402
- <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>
403
- </div>
404
- </div>
405
- </div>
406
- </div>`;
407
- }
408
- esc(str) {
409
- if (!str) return "";
410
- 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);
411
350
  }
412
351
  };
413
352
 
@@ -783,8 +722,8 @@ var ZerocostSDK = class {
783
722
  }
784
723
  this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
785
724
  try {
786
- const { display, dataCollection } = await this.core.request("/get-placements");
787
- this.widget.autoInjectWithConfig(display);
725
+ const { display, widget, dataCollection } = await this.core.request("/get-placements");
726
+ this.widget.autoInjectWithConfig(display, widget);
788
727
  if (dataCollection?.llm) {
789
728
  this.data.start(dataCollection.llm);
790
729
  }
package/dist/index.js CHANGED
@@ -117,23 +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", "sponsored-card", "sidebar-display", "tooltip-ad", "inline-text"];
120
+ var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
121
+ var AUTO_SLOT_ID = "zerocost-auto-slot";
121
122
  var WidgetModule = class {
122
123
  constructor(client) {
123
124
  this.client = client;
124
125
  }
125
126
  mounted = /* @__PURE__ */ new Map();
126
- async autoInjectWithConfig(display) {
127
+ async autoInjectWithConfig(display, widget) {
127
128
  try {
128
- const configs = this.normalizeConfigs(display);
129
- const chosen = this.pickOneFormat(configs);
130
- if (!chosen) {
131
- this.client.log("No ad formats enabled. Skipping injection.");
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.");
132
133
  return;
133
134
  }
134
- this.client.log(`Auto-inject: rendering "${chosen.format}" only.`);
135
- await this.mountSingleFormat(chosen.format, chosen.config);
136
- this.client.log("\u2713 Ad slot injected.");
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.");
137
138
  } catch (err) {
138
139
  this.client.log(`Widget autoInject error: ${err}`);
139
140
  }
@@ -141,21 +142,39 @@ var WidgetModule = class {
141
142
  async autoInject() {
142
143
  try {
143
144
  this.client.log("Fetching ad placements from server...");
144
- const { display } = await this.client.request("/get-placements");
145
- await this.autoInjectWithConfig(display);
145
+ const { display, widget } = await this.client.request("/get-placements");
146
+ await this.autoInjectWithConfig(display, widget);
146
147
  } catch (err) {
147
148
  this.client.log(`Widget autoInject error: ${err}`);
148
149
  }
149
150
  }
150
- pickOneFormat(configs) {
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);
151
162
  for (const fmt of FORMAT_PRIORITY) {
152
163
  const cfg = configs[fmt];
153
- if (cfg?.enabled) return { format: fmt, config: cfg };
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
+ }
154
173
  }
155
174
  return null;
156
175
  }
157
176
  normalizeConfigs(display) {
158
- if (display && "video-widget" in display) {
177
+ if (display && typeof display === "object" && "video-widget" in display) {
159
178
  return display;
160
179
  }
161
180
  const pos = display?.position || "bottom-right";
@@ -164,22 +183,57 @@ var WidgetModule = class {
164
183
  "video-widget": { position: pos, theme, autoplay: true, enabled: true },
165
184
  "tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
166
185
  "sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
186
+ "sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
167
187
  "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
168
188
  };
169
189
  }
170
- async mountSingleFormat(format, cfg) {
171
- const elId = `zerocost-${format}`;
172
- if (document.getElementById(elId)) return;
173
- if (format === "inline-text") return;
174
- const el = document.createElement("div");
175
- el.id = elId;
176
- el.setAttribute("data-zerocost", "");
177
- el.setAttribute("data-format", format);
178
- const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
179
- const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
180
- el.setAttribute("style", posStyle + maxW);
181
- document.body.appendChild(el);
182
- await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay ?? true });
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);
203
+ }
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);
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;
183
237
  }
184
238
  async mount(targetElementId, options = {}) {
185
239
  const el = document.getElementById(targetElementId);
@@ -188,36 +242,25 @@ var WidgetModule = class {
188
242
  const refreshMs = (options.refreshInterval ?? 30) * 1e3;
189
243
  const theme = options.theme || "dark";
190
244
  const format = options.format || "video-widget";
191
- const autoplay = options.autoplay ?? false;
245
+ const autoplay = options.autoplay ?? format === "video-widget";
192
246
  const render = async () => {
193
247
  try {
194
- const formatMap = {
195
- "video-widget": "video",
196
- "sponsored-card": "display",
197
- "sidebar-display": "display",
198
- "tooltip-ad": "native",
199
- "inline-text": "native"
200
- };
201
248
  const body = {
202
- format: formatMap[format] || format,
249
+ widget_style: format,
203
250
  theme,
204
- autoplay
251
+ autoplay,
252
+ position: options.position
205
253
  };
206
254
  const data = await this.client.request("/serve-widget", body);
207
255
  const ad = data.ad;
208
256
  if (!ad || !data.html) {
209
- 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}".`);
210
258
  el.innerHTML = "";
211
259
  return;
212
260
  }
213
261
  el.innerHTML = data.html;
214
262
  el.setAttribute("data-zerocost-ad-id", ad.id);
215
- const video = el.querySelector("video");
216
- if (video) {
217
- video.muted = true;
218
- video.play().catch(() => {
219
- });
220
- }
263
+ this.ensureVideoPlayback(el);
221
264
  const ctas = el.querySelectorAll("[data-zc-cta]");
222
265
  ctas.forEach((cta) => {
223
266
  cta.addEventListener("click", () => {
@@ -233,10 +276,7 @@ var WidgetModule = class {
233
276
  closeBtn.addEventListener("click", (e) => {
234
277
  e.preventDefault();
235
278
  e.stopPropagation();
236
- const mountInfo = this.mounted.get(targetElementId);
237
- if (mountInfo?.interval) clearInterval(mountInfo.interval);
238
- this.mounted.delete(targetElementId);
239
- el.remove();
279
+ this.unmount(targetElementId);
240
280
  });
241
281
  }
242
282
  } catch (err) {
@@ -247,138 +287,37 @@ var WidgetModule = class {
247
287
  const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
248
288
  this.mounted.set(targetElementId, { elementId: targetElementId, interval });
249
289
  }
250
- unmount(targetElementId) {
251
- const slot = this.mounted.get(targetElementId);
252
- if (slot) {
253
- if (slot.interval) clearInterval(slot.interval);
254
- const el = document.getElementById(targetElementId);
255
- if (el) el.remove();
256
- this.mounted.delete(targetElementId);
257
- }
258
- }
259
- unmountAll() {
260
- for (const id of this.mounted.keys()) this.unmount(id);
261
- }
262
- // ─── Format-specific renderers (matching SDK Playground previews exactly) ───
263
- buildFormatHtml(format, ad, theme, autoplay) {
264
- switch (format) {
265
- case "video-widget":
266
- return this.buildVideoWidget(ad, theme, autoplay);
267
- case "tooltip-ad":
268
- return this.buildTooltipAd(ad, theme);
269
- case "sponsored-card":
270
- return this.buildSponsoredCard(ad, theme);
271
- case "inline-text":
272
- return this.buildInlineText(ad, theme);
273
- default:
274
- 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;
275
303
  }
304
+ video.addEventListener("loadeddata", tryPlay, { once: true });
305
+ video.addEventListener("canplay", tryPlay, { once: true });
276
306
  }
277
- /**
278
- * Floating Video Widget — matches playground VideoWidgetPreview
279
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
280
- */
281
- buildVideoWidget(ad, theme, autoplay) {
282
- const isDark = theme === "dark";
283
- const border = isDark ? "#333" : "#e0e0e0";
284
- const videoSrc = ad.video_url || ad.image_url || "";
285
- const hasVideo = !!ad.video_url;
286
- const sponsor = ad.title || "Sponsor";
287
- const cta = ad.cta_text || "Learn More";
288
- return `
289
- <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"});">
290
- <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>
291
- ${hasVideo ? `<video src="${this.esc(videoSrc)}" autoplay muted loop playsinline preload="auto" 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;" />`}
292
- <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;">
293
- <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>
294
- <div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
295
- <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>
296
- </div>
297
- </div>`;
298
- }
299
- /**
300
- * Tooltip Ad — matches playground ContextualTextPreview
301
- * Small floating card with sponsored label, inline text, and link
302
- */
303
- buildTooltipAd(ad, theme) {
304
- const isDark = theme === "dark";
305
- const bg = isDark ? "#111" : "#ffffff";
306
- const fg = isDark ? "#fff" : "#111";
307
- const fgFaint = isDark ? "#888" : "#999";
308
- const border = isDark ? "#333" : "#e0e0e0";
309
- return `
310
- <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;position:relative;">
311
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
312
- <div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
313
- <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
314
- Sponsored
315
- </div>
316
- <p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
317
- ${this.esc(ad.description || ad.title)}
318
- <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>
319
- </p>
320
- </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());
321
311
  }
322
- /**
323
- * Sponsored Card — matches playground SponsoredCardPreview
324
- * Card with gradient header, icon, headline, description, CTA button
325
- */
326
- buildSponsoredCard(ad, theme) {
327
- const isDark = theme === "dark";
328
- const bg = isDark ? "#111" : "#ffffff";
329
- const fg = isDark ? "#fff" : "#111";
330
- const fgFaint = isDark ? "#888" : "#999";
331
- const border = isDark ? "#333" : "#e0e0e0";
332
- return `
333
- <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;position:relative;">
334
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;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:12px;line-height:1;">\u2715</button>
335
- ${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>`}
336
- <div style="padding:12px;">
337
- <div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
338
- <div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
339
- <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>
340
- </div>
341
- </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);
342
318
  }
343
- /**
344
- * Inline Text Ad matches playground InlineTextPreview
345
- * Card with sponsored header, icon, headline, body, CTA button
346
- * Designed to be injected inside content/chat streams
347
- */
348
- buildInlineText(ad, theme) {
349
- const isDark = theme === "dark";
350
- const bg = isDark ? "#141414" : "#fafafa";
351
- const fg = isDark ? "#fff" : "#111";
352
- const fgMuted = isDark ? "#aaa" : "#666";
353
- const fgFaint = isDark ? "#888" : "#999";
354
- const border = isDark ? "#2a2a2a" : "#d0d0d0";
355
- const headerBorder = isDark ? "#222" : "#e8e8e8";
356
- const btnBg = isDark ? "#fff" : "#111";
357
- const btnFg = isDark ? "#111" : "#fff";
358
- const initial = "\u25B2";
359
- const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
360
- return `
361
- <div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;position:relative;">
362
- <button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
363
- <div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
364
- <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
365
- <span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
366
- </div>
367
- <div style="padding:12px;">
368
- <div style="display:flex;gap:10px;">
369
- <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>
370
- <div style="flex:1;min-width:0;">
371
- <div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
372
- <div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
373
- <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>
374
- </div>
375
- </div>
376
- </div>
377
- </div>`;
378
- }
379
- esc(str) {
380
- if (!str) return "";
381
- 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);
382
321
  }
383
322
  };
384
323
 
@@ -754,8 +693,8 @@ var ZerocostSDK = class {
754
693
  }
755
694
  this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
756
695
  try {
757
- const { display, dataCollection } = await this.core.request("/get-placements");
758
- this.widget.autoInjectWithConfig(display);
696
+ const { display, widget, dataCollection } = await this.core.request("/get-placements");
697
+ this.widget.autoInjectWithConfig(display, widget);
759
698
  if (dataCollection?.llm) {
760
699
  this.data.start(dataCollection.llm);
761
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,41 +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>;
18
- private pickOneFormat;
21
+ private resolveSelectedWidget;
19
22
  private normalizeConfigs;
20
23
  private mountSingleFormat;
24
+ private ensureInlineTarget;
21
25
  mount(targetElementId: string, options?: {
22
26
  format?: string;
23
27
  refreshInterval?: number;
24
28
  theme?: string;
25
29
  autoplay?: boolean;
30
+ position?: string;
26
31
  }): Promise<void>;
32
+ private ensureVideoPlayback;
33
+ private clearAutoInjectedSlots;
27
34
  unmount(targetElementId: string): void;
28
35
  unmountAll(): void;
29
- private buildFormatHtml;
30
- /**
31
- * Floating Video Widget — matches playground VideoWidgetPreview
32
- * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
33
- */
34
- private buildVideoWidget;
35
- /**
36
- * Tooltip Ad — matches playground ContextualTextPreview
37
- * Small floating card with sponsored label, inline text, and link
38
- */
39
- private buildTooltipAd;
40
- /**
41
- * Sponsored Card — matches playground SponsoredCardPreview
42
- * Card with gradient header, icon, headline, description, CTA button
43
- */
44
- private buildSponsoredCard;
45
- /**
46
- * Inline Text Ad — matches playground InlineTextPreview
47
- * Card with sponsored header, icon, headline, body, CTA button
48
- * Designed to be injected inside content/chat streams
49
- */
50
- private buildInlineText;
51
- private esc;
52
36
  }
53
37
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerocost/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",