@active-reach/web-sdk 1.20.0 → 1.21.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.js CHANGED
@@ -2095,7 +2095,7 @@ function renderGame(ctx, subType) {
2095
2095
  fn(ctx);
2096
2096
  return true;
2097
2097
  }
2098
- const ASPECT = { "16:9": "16 / 9", "9:16": "9 / 16", "1:1": "1 / 1", "4:5": "4 / 5" };
2098
+ const ASPECT$1 = { "16:9": "16 / 9", "9:16": "9 / 16", "1:1": "1 / 1", "4:5": "4 / 5" };
2099
2099
  function buildVideo(ctx, aspect, autoplay, loop) {
2100
2100
  var _a;
2101
2101
  const { campaign, sanitizeUrl } = ctx;
@@ -2103,7 +2103,7 @@ function buildVideo(ctx, aspect, autoplay, loop) {
2103
2103
  const videoUrl = sanitizeUrl(ic.video_url || campaign.video_url || "");
2104
2104
  const poster = campaign.image_url ? sanitizeUrl(campaign.image_url) : null;
2105
2105
  const wrap = document.createElement("div");
2106
- wrap.style.cssText = `position:relative;width:100%;aspect-ratio:${ASPECT[aspect] || ASPECT["16:9"]};background:#0b1220;border-radius:14px;overflow:hidden;`;
2106
+ wrap.style.cssText = `position:relative;width:100%;aspect-ratio:${ASPECT$1[aspect] || ASPECT$1["16:9"]};background:#0b1220;border-radius:14px;overflow:hidden;`;
2107
2107
  if (videoUrl) {
2108
2108
  const vid = document.createElement("video");
2109
2109
  vid.src = videoUrl;
@@ -2272,11 +2272,179 @@ function renderVideoInline(ctx, target) {
2272
2272
  target.appendChild(banner);
2273
2273
  return true;
2274
2274
  }
2275
+ const HEX = /^#[0-9a-fA-F]{3,8}$/;
2276
+ function safeColor(c, fallback) {
2277
+ return c && HEX.test(c) ? c : fallback;
2278
+ }
2279
+ const RADIUS = { none: "0", lg: "12px", xl: "16px", "2xl": "20px" };
2280
+ const ASPECT = { "16:9": "16 / 9", "4:3": "4 / 3", "21:9": "21 / 9" };
2281
+ function cardHeadline(c) {
2282
+ return c.headline ?? c.title ?? "";
2283
+ }
2284
+ function cardSub(c) {
2285
+ return c.subhead ?? c.body ?? "";
2286
+ }
2287
+ function cardCta(c) {
2288
+ return c.cta_label ?? c.cta_text ?? "";
2289
+ }
2290
+ function cardUrl(c) {
2291
+ if (c.cta_url) return c.cta_url;
2292
+ const t = c.cta_target;
2293
+ if (t && t.value) return t.type === "category" ? `?category=${encodeURIComponent(t.value)}` : t.value;
2294
+ return "";
2295
+ }
2296
+ function renderHeroInline(ctx, target) {
2297
+ const { campaign, sanitizeUrl, trackEvent } = ctx;
2298
+ const ic = campaign.interactive_config || {};
2299
+ const cards = Array.isArray(ic.cards) ? ic.cards : [];
2300
+ if (cards.length === 0) return false;
2301
+ const chrome = ic.chrome || {};
2302
+ const accent = safeColor(
2303
+ chrome.accent && chrome.accent !== "brand" ? chrome.accent : campaign.background_color,
2304
+ "#4169e1"
2305
+ );
2306
+ const radius = RADIUS[chrome.radius ?? "2xl"] ?? RADIUS["2xl"];
2307
+ const variant = chrome.variant ?? "hero_fullbleed";
2308
+ if (variant === "announcement_bar") {
2309
+ const c = cards[0];
2310
+ const bar = document.createElement("div");
2311
+ bar.style.cssText = `display:flex;align-items:center;justify-content:center;gap:10px;background:${accent};color:#fff;padding:8px 14px;font:600 13px/1.3 Inter,system-ui,sans-serif;text-align:center;`;
2312
+ bar.textContent = [cardHeadline(c), cardSub(c)].filter(Boolean).join(" · ");
2313
+ const url = sanitizeUrl(cardUrl(c));
2314
+ if (url && cardCta(c)) {
2315
+ const a = document.createElement("span");
2316
+ a.style.cssText = "text-decoration:underline;cursor:pointer;white-space:nowrap;";
2317
+ a.textContent = cardCta(c);
2318
+ a.addEventListener("click", () => {
2319
+ var _a;
2320
+ trackEvent(campaign.id, "clicked");
2321
+ (_a = ctx.navigate) == null ? void 0 : _a.call(ctx, url);
2322
+ });
2323
+ bar.appendChild(a);
2324
+ }
2325
+ target.appendChild(bar);
2326
+ trackEvent(campaign.id, "impression");
2327
+ return true;
2328
+ }
2329
+ const root = document.createElement("div");
2330
+ root.style.cssText = `position:relative;width:100%;overflow:hidden;border-radius:${radius};box-shadow:0 1px 2px rgba(16,24,40,0.06);${chrome.aspect_ratio && ASPECT[chrome.aspect_ratio] ? `aspect-ratio:${ASPECT[chrome.aspect_ratio]};` : "min-height:300px;"}`;
2331
+ root.setAttribute("data-campaign-id", campaign.id);
2332
+ const track = document.createElement("div");
2333
+ track.style.cssText = "display:flex;height:100%;width:100%;transition:transform 0.5s ease;";
2334
+ root.appendChild(track);
2335
+ const overlayCss = chrome.overlay === "none" ? "" : chrome.overlay === "full-scrim" ? "background:linear-gradient(0deg,rgba(0,0,0,0.55),rgba(0,0,0,0.25));" : "background:linear-gradient(to top right,rgba(0,0,0,0.7),rgba(0,0,0,0.25) 45%,transparent);";
2336
+ const justify = chrome.text_position === "center" ? "center" : "flex-end";
2337
+ const align = chrome.text_position === "bottom-left" || !chrome.text_position ? "flex-start" : "center";
2338
+ cards.forEach((c, i) => {
2339
+ const slide = document.createElement("div");
2340
+ slide.style.cssText = "position:relative;flex:0 0 100%;width:100%;height:100%;min-height:inherit;background:#f1f5f9;";
2341
+ const videoUrl = sanitizeUrl(c.video_url ?? "");
2342
+ const isVideo = !!videoUrl || c.media_type === "video";
2343
+ const mediaUrl = isVideo ? videoUrl || sanitizeUrl(c.media_url ?? "") : sanitizeUrl(c.image_url ?? c.media_url ?? "");
2344
+ if (mediaUrl) {
2345
+ if (isVideo) {
2346
+ const v = document.createElement("video");
2347
+ v.src = mediaUrl;
2348
+ v.autoplay = true;
2349
+ v.muted = true;
2350
+ v.loop = true;
2351
+ v.playsInline = true;
2352
+ v.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
2353
+ slide.appendChild(v);
2354
+ } else {
2355
+ const img = document.createElement("div");
2356
+ img.style.cssText = `position:absolute;inset:0;background:url("${mediaUrl}") center/cover no-repeat;`;
2357
+ slide.appendChild(img);
2358
+ }
2359
+ }
2360
+ if (overlayCss) {
2361
+ const scrim = document.createElement("div");
2362
+ scrim.style.cssText = `position:absolute;inset:0;${overlayCss}`;
2363
+ slide.appendChild(scrim);
2364
+ }
2365
+ const content = document.createElement("div");
2366
+ content.style.cssText = `position:absolute;inset:0;display:flex;flex-direction:column;justify-content:${justify};align-items:${align};gap:6px;padding:24px;text-align:${align === "center" ? "center" : "left"};`;
2367
+ const h = cardHeadline(c);
2368
+ if (h) {
2369
+ const head = document.createElement("h2");
2370
+ head.textContent = h;
2371
+ head.style.cssText = "margin:0;color:#fff;font:800 24px/1.2 Inter Tight,Inter,system-ui,sans-serif;letter-spacing:-0.02em;text-shadow:0 1px 8px rgba(0,0,0,0.35);max-width:90%;";
2372
+ content.appendChild(head);
2373
+ }
2374
+ const s = cardSub(c);
2375
+ if (s) {
2376
+ const sub = document.createElement("p");
2377
+ sub.textContent = s;
2378
+ sub.style.cssText = "margin:0;color:rgba(255,255,255,0.92);font:500 14px/1.4 Inter,system-ui,sans-serif;text-shadow:0 1px 6px rgba(0,0,0,0.3);max-width:90%;";
2379
+ content.appendChild(sub);
2380
+ }
2381
+ const cta = cardCta(c);
2382
+ const url = sanitizeUrl(cardUrl(c));
2383
+ if (cta && chrome.cta_style !== "none") {
2384
+ const btn = document.createElement("button");
2385
+ btn.textContent = cta;
2386
+ btn.style.cssText = chrome.cta_style === "underline" ? `margin-top:6px;background:none;border:none;color:#fff;font:700 14px Inter,system-ui,sans-serif;text-decoration:underline;cursor:pointer;padding:0;` : `margin-top:8px;background:${accent};color:#fff;border:none;border-radius:999px;padding:9px 18px;font:700 13px Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.2);`;
2387
+ btn.addEventListener("click", () => {
2388
+ var _a;
2389
+ trackEvent(campaign.id, "clicked", { stepId: `card_${i}` });
2390
+ if (url) (_a = ctx.navigate) == null ? void 0 : _a.call(ctx, url);
2391
+ });
2392
+ content.appendChild(btn);
2393
+ }
2394
+ slide.appendChild(content);
2395
+ track.appendChild(slide);
2396
+ });
2397
+ const dotEls = [];
2398
+ let activeIdx = 0;
2399
+ const goto = (idx) => {
2400
+ activeIdx = (idx % cards.length + cards.length) % cards.length;
2401
+ track.style.transform = `translateX(-${activeIdx * 100}%)`;
2402
+ dotEls.forEach((d, i) => {
2403
+ d.style.width = i === activeIdx ? "18px" : "6px";
2404
+ d.style.background = i === activeIdx ? accent : "rgba(255,255,255,0.7)";
2405
+ });
2406
+ };
2407
+ const dots = document.createElement("div");
2408
+ dots.style.cssText = "position:absolute;bottom:8px;left:50%;transform:translateX(-50%);display:flex;gap:6px;z-index:2;";
2409
+ if (cards.length > 1) {
2410
+ cards.forEach((_, i) => {
2411
+ const dot = document.createElement("span");
2412
+ dot.style.cssText = "width:6px;height:6px;border-radius:999px;background:rgba(255,255,255,0.7);transition:all 0.25s;cursor:pointer;";
2413
+ dot.addEventListener("click", () => goto(i));
2414
+ dotEls.push(dot);
2415
+ dots.appendChild(dot);
2416
+ });
2417
+ root.appendChild(dots);
2418
+ }
2419
+ target.appendChild(root);
2420
+ goto(0);
2421
+ if (cards.length > 1 && chrome.loop !== false) {
2422
+ const ms = typeof chrome.autoplay_ms === "number" && chrome.autoplay_ms >= 1500 ? chrome.autoplay_ms : 4e3;
2423
+ let timer = window.setInterval(() => goto(activeIdx + 1), ms);
2424
+ root.addEventListener("mouseenter", () => window.clearInterval(timer));
2425
+ root.addEventListener("mouseleave", () => {
2426
+ timer = window.setInterval(() => goto(activeIdx + 1), ms);
2427
+ });
2428
+ }
2429
+ trackEvent(campaign.id, "impression");
2430
+ return true;
2431
+ }
2275
2432
  const STYLE_ID = "aegis-chat-styles";
2276
2433
  const POLL_INTERVAL_MS = 5e3;
2434
+ let currentLauncher = null;
2435
+ function getCurrentLauncher() {
2436
+ return currentLauncher;
2437
+ }
2438
+ function openChat(prefill) {
2439
+ currentLauncher == null ? void 0 : currentLauncher.openPanel(prefill);
2440
+ }
2441
+ function closeChat() {
2442
+ currentLauncher == null ? void 0 : currentLauncher.closePanel();
2443
+ }
2277
2444
  const ATTENTION_DELAY_MS = 4500;
2278
2445
  class AegisChat {
2279
2446
  constructor(config) {
2447
+ this.sessionResumed = false;
2280
2448
  this.open = false;
2281
2449
  this.initialized = false;
2282
2450
  this.unread = 0;
@@ -2294,12 +2462,14 @@ class AegisChat {
2294
2462
  this.icon = config.icon ?? "sparkle";
2295
2463
  this.logoUrl = config.logoUrl;
2296
2464
  this.position = config.position ?? "bottom-right";
2465
+ this.displayMode = config.displayMode ?? "bubble";
2297
2466
  this.agentPersona = config.agentPersona;
2298
2467
  this.quickReplies = config.quickReplies;
2299
2468
  this.onSessionStart = config.onSessionStart;
2300
2469
  this.onSessionEnd = config.onSessionEnd;
2301
2470
  this.onMessageSent = config.onMessageSent;
2302
2471
  this.anonymousId = this.readAnonId();
2472
+ this.contactId = this.contactId ?? this.readStoredContactId();
2303
2473
  }
2304
2474
  // ── Lifecycle ────────────────────────────────────────────────────────────
2305
2475
  initialize() {
@@ -2307,7 +2477,8 @@ class AegisChat {
2307
2477
  this.initialized = true;
2308
2478
  this.injectStyles();
2309
2479
  this.mount();
2310
- this.scheduleAttention();
2480
+ currentLauncher = this;
2481
+ if (this.displayMode !== "nav") this.scheduleAttention();
2311
2482
  }
2312
2483
  /** Called by the runtime when the visitor identifies. */
2313
2484
  updateContactId(contactId) {
@@ -2330,7 +2501,7 @@ class AegisChat {
2330
2501
  (_c = this.bubble) == null ? void 0 : _c.classList.add("aegis-chat-bubble--hidden");
2331
2502
  this.clearUnread();
2332
2503
  if (prefill && this.input) this.input.value = prefill;
2333
- void this.refreshHistory();
2504
+ void this.resumeSession();
2334
2505
  this.startPolling();
2335
2506
  (_d = this.input) == null ? void 0 : _d.focus();
2336
2507
  }
@@ -2355,13 +2526,22 @@ class AegisChat {
2355
2526
  this.stopPolling();
2356
2527
  (_a = this.root) == null ? void 0 : _a.remove();
2357
2528
  this.initialized = false;
2529
+ if (currentLauncher === this) currentLauncher = null;
2358
2530
  }
2359
2531
  // ── Send ─────────────────────────────────────────────────────────────────
2360
- async send(text) {
2532
+ async send(text, media) {
2361
2533
  var _a;
2362
- const message = text.trim();
2363
- if (!message) return;
2364
- this.appendBubble({ id: `local_${Date.now()}`, sender: "customer", role: "user", content: message });
2534
+ const caption2 = text.trim();
2535
+ if (!caption2 && !media) return;
2536
+ const message = caption2 || (media ? media.type === "image" ? "Sent a photo 📷" : "Sent a file 📎" : "");
2537
+ this.appendBubble({
2538
+ id: `local_${Date.now()}`,
2539
+ sender: "customer",
2540
+ role: "user",
2541
+ content: message,
2542
+ media_url: (media == null ? void 0 : media.url) ?? null,
2543
+ media_type: (media == null ? void 0 : media.type) ?? null
2544
+ });
2365
2545
  this.pendingEcho.push(message);
2366
2546
  if (this.input) {
2367
2547
  this.input.value = "";
@@ -2377,6 +2557,8 @@ class AegisChat {
2377
2557
  channel: this.channel,
2378
2558
  anonymous_id: this.anonymousId,
2379
2559
  contact_id: this.contactId,
2560
+ media_url: media == null ? void 0 : media.url,
2561
+ media_type: media == null ? void 0 : media.type,
2380
2562
  // Active Web Chat — binds the campaign-configured persona. Optional;
2381
2563
  // the backend falls back to the tenant default if unknown/absent
2382
2564
  // (honored server-side in Phase E). JSON.stringify drops it if absent.
@@ -2395,6 +2577,7 @@ class AegisChat {
2395
2577
  } catch {
2396
2578
  }
2397
2579
  this.contactId = data.contact_id;
2580
+ this.persistContactId(data.contact_id);
2398
2581
  this.sseTicket = data.sse_ticket;
2399
2582
  this.connectSSE(data.sse_ticket);
2400
2583
  void this.refreshHistory();
@@ -2403,6 +2586,51 @@ class AegisChat {
2403
2586
  this.appendSystem("Sorry — your message could not be delivered. Please try again.");
2404
2587
  }
2405
2588
  }
2589
+ // ── Media attachments ─────────────────────────────────────────────────────
2590
+ /** Validate + upload a picked file, then send it as a media message. */
2591
+ async handleFile(file) {
2592
+ var _a;
2593
+ const allowed = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf"]);
2594
+ if (!allowed.has(file.type)) {
2595
+ this.appendSystem("That file type isn’t supported (images or PDF only).");
2596
+ return;
2597
+ }
2598
+ if (file.size > 8 * 1024 * 1024) {
2599
+ this.appendSystem("That file is too large (max 8 MB).");
2600
+ return;
2601
+ }
2602
+ try {
2603
+ const data = await this.fileToBase64(file);
2604
+ const res = await fetch(`${this.apiHost}/v1/chat/upload`, {
2605
+ method: "POST",
2606
+ headers: { "Content-Type": "application/json", "X-Aegis-Write-Key": this.writeKey },
2607
+ body: JSON.stringify({ data, content_type: file.type, filename: file.name })
2608
+ });
2609
+ if (!res.ok) {
2610
+ this.log("upload failed", res.status);
2611
+ this.appendSystem("Sorry — that attachment couldn’t be uploaded.");
2612
+ return;
2613
+ }
2614
+ const out = await res.json();
2615
+ await this.send(((_a = this.input) == null ? void 0 : _a.value) ?? "", { url: out.media_url, type: out.media_type });
2616
+ } catch (err) {
2617
+ this.log("upload error", err);
2618
+ this.appendSystem("Sorry — that attachment couldn’t be uploaded.");
2619
+ }
2620
+ }
2621
+ /** Read a File as bare base64 (strip the `data:…;base64,` prefix). */
2622
+ fileToBase64(file) {
2623
+ return new Promise((resolve, reject) => {
2624
+ const reader = new FileReader();
2625
+ reader.onload = () => {
2626
+ const result = String(reader.result || "");
2627
+ const comma = result.indexOf(",");
2628
+ resolve(comma >= 0 ? result.slice(comma + 1) : result);
2629
+ };
2630
+ reader.onerror = () => reject(reader.error);
2631
+ reader.readAsDataURL(file);
2632
+ });
2633
+ }
2406
2634
  // ── Reply transport: SSE nudge + history refetch (poll fallback) ──────────
2407
2635
  connectSSE(ticket) {
2408
2636
  if (!this.open) return;
@@ -2540,6 +2768,21 @@ class AegisChat {
2540
2768
  void this.send(input.value);
2541
2769
  }
2542
2770
  });
2771
+ const fileInput = document.createElement("input");
2772
+ fileInput.type = "file";
2773
+ fileInput.accept = "image/*,application/pdf";
2774
+ fileInput.className = "aegis-chat-fileinput";
2775
+ fileInput.addEventListener("change", () => {
2776
+ const f = fileInput.files && fileInput.files[0];
2777
+ if (f) void this.handleFile(f);
2778
+ fileInput.value = "";
2779
+ });
2780
+ const attachBtn = document.createElement("button");
2781
+ attachBtn.type = "button";
2782
+ attachBtn.className = "aegis-chat-attach";
2783
+ attachBtn.setAttribute("aria-label", "Attach a file");
2784
+ attachBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
2785
+ attachBtn.addEventListener("click", () => fileInput.click());
2543
2786
  const sendBtn = document.createElement("button");
2544
2787
  sendBtn.type = "submit";
2545
2788
  sendBtn.className = "aegis-chat-send";
@@ -2549,8 +2792,10 @@ class AegisChat {
2549
2792
  e.preventDefault();
2550
2793
  void this.send(input.value);
2551
2794
  });
2795
+ composer.appendChild(attachBtn);
2552
2796
  composer.appendChild(input);
2553
2797
  composer.appendChild(sendBtn);
2798
+ composer.appendChild(fileInput);
2554
2799
  panel.appendChild(header);
2555
2800
  panel.appendChild(thread);
2556
2801
  panel.appendChild(composer);
@@ -2570,8 +2815,10 @@ class AegisChat {
2570
2815
  tip.appendChild(tipClose);
2571
2816
  tip.addEventListener("click", () => this.openPanel());
2572
2817
  root.appendChild(panel);
2573
- root.appendChild(tip);
2574
- root.appendChild(bubble);
2818
+ if (this.displayMode !== "nav") {
2819
+ root.appendChild(tip);
2820
+ root.appendChild(bubble);
2821
+ }
2575
2822
  document.body.appendChild(root);
2576
2823
  this.greetingTip = tip;
2577
2824
  this.root = root;
@@ -2609,6 +2856,26 @@ class AegisChat {
2609
2856
  if (msg.id) this.renderedIds.add(msg.id);
2610
2857
  const row = document.createElement("div");
2611
2858
  row.className = `aegis-chat-msg aegis-chat-msg--${msg.sender}`;
2859
+ if (msg.media_url) {
2860
+ if (msg.media_type === "image") {
2861
+ const img = document.createElement("img");
2862
+ img.className = "aegis-chat-media-img";
2863
+ img.src = msg.media_url;
2864
+ img.alt = "attachment";
2865
+ img.loading = "lazy";
2866
+ const url = msg.media_url;
2867
+ img.addEventListener("click", () => window.open(url, "_blank", "noopener"));
2868
+ row.appendChild(img);
2869
+ } else {
2870
+ const a = document.createElement("a");
2871
+ a.className = "aegis-chat-media-file";
2872
+ a.href = msg.media_url;
2873
+ a.target = "_blank";
2874
+ a.rel = "noopener noreferrer";
2875
+ a.textContent = "📎 Attachment";
2876
+ row.appendChild(a);
2877
+ }
2878
+ }
2612
2879
  if (msg.content) {
2613
2880
  const bubble = document.createElement("div");
2614
2881
  bubble.className = "aegis-chat-bubbletext";
@@ -2700,6 +2967,54 @@ class AegisChat {
2700
2967
  return void 0;
2701
2968
  }
2702
2969
  }
2970
+ readStoredContactId() {
2971
+ if (typeof document === "undefined") return void 0;
2972
+ try {
2973
+ return new Storage().get("aegis_chat_cid") ?? void 0;
2974
+ } catch {
2975
+ return void 0;
2976
+ }
2977
+ }
2978
+ persistContactId(id) {
2979
+ if (!id || typeof document === "undefined") return;
2980
+ try {
2981
+ new Storage().set("aegis_chat_cid", id, 365);
2982
+ } catch {
2983
+ }
2984
+ }
2985
+ /** Restore a returning visitor's thread on open. Resolves an existing contact
2986
+ * + mints a per-session ticket WITHOUT sending a message (history is ticket-
2987
+ * gated). No-ops for brand-new visitors and once a ticket is already held. */
2988
+ async resumeSession() {
2989
+ if (this.sseTicket) {
2990
+ void this.refreshHistory();
2991
+ return;
2992
+ }
2993
+ if (this.sessionResumed) return;
2994
+ this.sessionResumed = true;
2995
+ if (!this.contactId && !this.anonymousId) return;
2996
+ try {
2997
+ const res = await fetch(`${this.apiHost}/v1/chat/session`, {
2998
+ method: "POST",
2999
+ headers: { "Content-Type": "application/json", "X-Aegis-Write-Key": this.writeKey },
3000
+ body: JSON.stringify({
3001
+ anonymous_id: this.anonymousId,
3002
+ contact_id: this.contactId,
3003
+ channel: this.channel
3004
+ })
3005
+ });
3006
+ if (!res.ok) return;
3007
+ const data = await res.json();
3008
+ if (!data.contact_id || !data.sse_ticket) return;
3009
+ this.contactId = data.contact_id;
3010
+ this.sseTicket = data.sse_ticket;
3011
+ this.persistContactId(data.contact_id);
3012
+ await this.refreshHistory();
3013
+ if (this.open) this.connectSSE(data.sse_ticket);
3014
+ } catch (err) {
3015
+ this.log("resume session failed", err);
3016
+ }
3017
+ }
2703
3018
  // ── Typing indicator ───────────────────────────────────────────────────────
2704
3019
  showTyping() {
2705
3020
  if (!this.thread || this.typingEl) return;
@@ -2800,11 +3115,19 @@ const CHAT_CSS = `
2800
3115
  .aegis-chat-input{flex:1;resize:none;border:1px solid #e2e8f0;border-radius:12px;padding:9px 12px;font-size:14px;font-family:inherit;max-height:96px;outline:none}
2801
3116
  .aegis-chat-input:focus{border-color:var(--aegis-chat-accent,#4169e1)}
2802
3117
  .aegis-chat-send{width:40px;height:40px;border-radius:9999px;border:none;background:var(--aegis-chat-accent,#4169e1);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto}
3118
+ .aegis-chat-attach{width:38px;height:38px;border-radius:9999px;border:none;background:transparent;color:#64748b;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto;transition:background .15s ease,color .15s ease}
3119
+ .aegis-chat-attach:hover{background:#f1f5f9;color:var(--aegis-chat-accent,#4169e1)}
3120
+ .aegis-chat-fileinput{display:none}
3121
+ .aegis-chat-media-img{max-width:200px;max-height:200px;border-radius:12px;object-fit:cover;cursor:pointer;display:block}
3122
+ .aegis-chat-media-file{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-size:13px;color:#0f172a;text-decoration:none;max-width:220px}
3123
+ .aegis-chat-msg--customer .aegis-chat-media-file{background:rgba(255,255,255,.18);border-color:transparent;color:#fff}
2803
3124
  /* Mobile: raise the bubble above the storefront's full-width cart bar so they
2804
3125
  never overlap; widen the panel and reduce the tooltip width. */
2805
- @media (max-width:480px){
3126
+ @media (max-width:767px){
2806
3127
  .aegis-chat-root{bottom:84px;right:16px}
2807
- .aegis-chat-panel{width:calc(100vw - 24px);height:calc(100vh - 140px)}
3128
+ /* Full-page chat on mobile — the open panel covers the whole viewport (incl.
3129
+ the bottom nav) like a native chat screen, instead of a floating card. */
3130
+ .aegis-chat-panel{position:fixed;inset:0;left:0;right:0;width:100vw;height:100vh;height:100dvh;max-width:none;max-height:none;border-radius:0}
2808
3131
  .aegis-chat-tip{max-width:200px}
2809
3132
  }
2810
3133
  `;
@@ -3376,6 +3699,10 @@ function renderActiveWebChat(ctx, creds) {
3376
3699
  icon: icon === "sparkle" || icon === "chat" || icon === "logo" ? icon : void 0,
3377
3700
  logoUrl: logo ? ctx.sanitizeUrl(logo) ?? void 0 : void 0,
3378
3701
  position: ic.chat_position === "bottom-left" ? "bottom-left" : void 0,
3702
+ // 'nav' = headless (no bubble); the host opens it via aegis.chat.open() or a
3703
+ // client_trigger. Defaults to 'bubble'. Per-device override via device_type
3704
+ // targeting on the campaign (e.g. a mobile campaign with chat_display_mode='nav').
3705
+ displayMode: ic.chat_display_mode === "nav" ? "nav" : void 0,
3379
3706
  agentPersona: str(ic.chat_agent_persona),
3380
3707
  quickReplies: Array.isArray(ic.chat_quick_replies) ? ic.chat_quick_replies.filter((q) => typeof q === "string") : void 0,
3381
3708
  // Bridge the engine's session lifecycle to campaign analytics. Per the
@@ -3436,6 +3763,7 @@ const _AegisInAppManager = class _AegisInAppManager {
3436
3763
  constructor(config) {
3437
3764
  this.campaigns = [];
3438
3765
  this.displayedCampaigns = /* @__PURE__ */ new Set();
3766
+ this._displaySizeMul = 1;
3439
3767
  this.suppressedUntil = /* @__PURE__ */ new Map();
3440
3768
  this.isInitialized = false;
3441
3769
  this.reconnectAttempts = 0;
@@ -4015,7 +4343,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4015
4343
  const list = eligibleByCategory.get(key);
4016
4344
  if (!list || list.length === 0) return;
4017
4345
  slot.querySelectorAll(":scope > [data-aegis-slot-default]").forEach((d) => d.remove());
4018
- const rotateMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
4346
+ const attrMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
4347
+ const declaredMs = list.reduce((m2, c) => {
4348
+ var _a;
4349
+ const v = Number((_a = c.interactive_config) == null ? void 0 : _a.slot_rotate_ms);
4350
+ return Number.isFinite(v) && v > m2 ? v : m2;
4351
+ }, 0);
4352
+ const rotateMs = attrMs > 0 ? attrMs : declaredMs;
4019
4353
  if (rotateMs > 0 && list.length > 1) {
4020
4354
  this.renderRotatingSlot(slot, list, rotateMs);
4021
4355
  } else {
@@ -4198,6 +4532,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4198
4532
  this.displayedCampaigns.add(campaign.id);
4199
4533
  this.addAnimationStyles();
4200
4534
  const ic = campaign.interactive_config || {};
4535
+ const inlineMul = this._sizeMul(ic);
4536
+ if (inlineMul !== 1) {
4537
+ const sizer = document.createElement("div");
4538
+ sizer.style.cssText = `transform: scale(${inlineMul}); transform-origin: top center;`;
4539
+ target.appendChild(sizer);
4540
+ target = sizer;
4541
+ }
4201
4542
  const { bg, text } = this._surfacePalette(campaign);
4202
4543
  const submitUrl = options == null ? void 0 : options.submitUrl;
4203
4544
  const _formFields = Array.isArray(ic.form_fields) ? ic.form_fields : null;
@@ -4214,70 +4555,73 @@ const _AegisInAppManager = class _AegisInAppManager {
4214
4555
  }
4215
4556
  return;
4216
4557
  }
4217
- switch (campaign.sub_type) {
4218
- case "star_rating":
4219
- rendered = this.renderStarRatingSlot(
4220
- campaign,
4221
- ic,
4222
- bg,
4223
- text,
4224
- target,
4225
- submitUrl
4226
- );
4227
- break;
4228
- case "nps_survey":
4229
- rendered = this.renderNPSSurveySlot(
4230
- campaign,
4231
- ic,
4232
- bg,
4233
- text,
4234
- target,
4235
- submitUrl
4236
- );
4237
- break;
4238
- // Parity tracker — built-in embedded gamification renderers (no
4239
- // AegisMessageRuntime callback needed, so they work on the bill).
4240
- case "spin_wheel":
4241
- rendered = this.renderSpinWheelSlot(campaign, ic, bg, text, target);
4242
- break;
4243
- case "quick_poll":
4244
- rendered = this.renderQuickPollSlot(campaign, ic, bg, text, target);
4245
- break;
4246
- case "carousel_cards":
4247
- rendered = this.renderCarouselCardsSlot(campaign, ic, bg, text, target);
4248
- break;
4249
- case "countdown_offer":
4250
- rendered = this.renderCountdownSlot(campaign, ic, bg, text, target);
4251
- break;
4252
- case "scratch_card":
4253
- rendered = this.renderScratchCardSlot(campaign, ic, bg, text, target);
4254
- break;
4255
- case "custom_html":
4256
- rendered = this.renderCustomHtmlSlot(campaign, ic, bg, text, target);
4257
- break;
4258
- case "stories":
4259
- rendered = renderStoriesRings(this.buildRenderContext(campaign), target);
4260
- break;
4261
- case "video":
4262
- rendered = renderVideoInline(this.buildRenderContext(campaign), target);
4263
- break;
4264
- case "progress_bar":
4265
- rendered = renderProgressBarInline(this.buildRenderContext(campaign), target);
4266
- break;
4267
- case "quiz":
4268
- rendered = this.renderQuizSlot(campaign, ic, bg, text, target);
4269
- if (!rendered) {
4558
+ if (campaign.widget_category === "hero") {
4559
+ rendered = renderHeroInline(this.buildRenderContext(campaign), target);
4560
+ } else
4561
+ switch (campaign.sub_type) {
4562
+ case "star_rating":
4563
+ rendered = this.renderStarRatingSlot(
4564
+ campaign,
4565
+ ic,
4566
+ bg,
4567
+ text,
4568
+ target,
4569
+ submitUrl
4570
+ );
4571
+ break;
4572
+ case "nps_survey":
4573
+ rendered = this.renderNPSSurveySlot(
4574
+ campaign,
4575
+ ic,
4576
+ bg,
4577
+ text,
4578
+ target,
4579
+ submitUrl
4580
+ );
4581
+ break;
4582
+ // Parity tracker built-in embedded gamification renderers (no
4583
+ // AegisMessageRuntime callback needed, so they work on the bill).
4584
+ case "spin_wheel":
4585
+ rendered = this.renderSpinWheelSlot(campaign, ic, bg, text, target);
4586
+ break;
4587
+ case "quick_poll":
4588
+ rendered = this.renderQuickPollSlot(campaign, ic, bg, text, target);
4589
+ break;
4590
+ case "carousel_cards":
4591
+ rendered = this.renderCarouselCardsSlot(campaign, ic, bg, text, target);
4592
+ break;
4593
+ case "countdown_offer":
4594
+ rendered = this.renderCountdownSlot(campaign, ic, bg, text, target);
4595
+ break;
4596
+ case "scratch_card":
4597
+ rendered = this.renderScratchCardSlot(campaign, ic, bg, text, target);
4598
+ break;
4599
+ case "custom_html":
4600
+ rendered = this.renderCustomHtmlSlot(campaign, ic, bg, text, target);
4601
+ break;
4602
+ case "stories":
4603
+ rendered = renderStoriesRings(this.buildRenderContext(campaign), target);
4604
+ break;
4605
+ case "video":
4606
+ rendered = renderVideoInline(this.buildRenderContext(campaign), target);
4607
+ break;
4608
+ case "progress_bar":
4609
+ rendered = renderProgressBarInline(this.buildRenderContext(campaign), target);
4610
+ break;
4611
+ case "quiz":
4612
+ rendered = this.renderQuizSlot(campaign, ic, bg, text, target);
4613
+ if (!rendered) {
4614
+ rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4615
+ }
4616
+ break;
4617
+ // Remaining sub_types (sticky_bar, progress_bar,
4618
+ // product_recommendation) render as a generic card on the bill slot
4619
+ // (overlay path handles their full UX); dedicated slot variants get added
4620
+ // here as merchants need them.
4621
+ default:
4270
4622
  rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4271
- }
4272
- break;
4273
- // Remaining sub_types (sticky_bar, progress_bar,
4274
- // product_recommendation) render as a generic card on the bill slot
4275
- // (overlay path handles their full UX); dedicated slot variants get added
4276
- // here as merchants need them.
4277
- default:
4278
- rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4279
- break;
4280
- }
4623
+ break;
4624
+ }
4281
4625
  if (!rendered) return;
4282
4626
  if (!(options == null ? void 0 : options.deferImpression)) {
4283
4627
  this.trackEvent(campaign.id, "impression");
@@ -4618,6 +4962,8 @@ const _AegisInAppManager = class _AegisInAppManager {
4618
4962
  const hubImg = cfgStr("spin_hub_image_url");
4619
4963
  const wheelBgImg = cfgStr("wheel_background_url");
4620
4964
  const pointerImg = cfgStr("pointer_image_url");
4965
+ const pointerCenter = (cfgStr("spin_pointer_position") || "top") === "center";
4966
+ const pointerBase = pointerCenter ? "translate(-50%, -100%)" : "translateX(-50%)";
4621
4967
  const isUrl = (s) => /^https?:\/\//i.test(s);
4622
4968
  const now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
4623
4969
  let rotation = 0;
@@ -4677,7 +5023,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4677
5023
  vibrate(18);
4678
5024
  };
4679
5025
  const body = document.createElement("div");
4680
- body.style.cssText = `padding: ${st.padY ?? 22}px ${st.padX ?? 20}px 20px; display: flex; flex-direction: column; align-items: center; gap: 16px;`;
5026
+ body.style.cssText = `padding: ${st.padY ?? 22}px ${st.padX ?? 20}px 20px; display: flex; flex-direction: column; align-items: center; gap: 10px;`;
4681
5027
  if (campaign.title) {
4682
5028
  const t = document.createElement("div");
4683
5029
  t.style.cssText = `font-size: 18px; font-weight: 800; letter-spacing: -0.01em; text-align: center; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
@@ -4695,11 +5041,55 @@ const _AegisInAppManager = class _AegisInAppManager {
4695
5041
  const wheelWrap = document.createElement("div");
4696
5042
  wheelWrap.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; filter: drop-shadow(0 10px 22px rgba(0,0,0,0.30));`;
4697
5043
  const pointer = document.createElement("div");
4698
- pointer.style.cssText = pointerImg ? `position: absolute; left: 50%; top: -10px; transform: translateX(-50%); transform-origin: 50% 0; width: 26px; height: 30px; background: center / contain no-repeat url("${pointerImg}"); z-index: 5; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));` : `position: absolute; left: 50%; top: -7px; transform: translateX(-50%); transform-origin: 50% 0; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 16px solid ${pointerColor}; z-index: 5; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));`;
5044
+ if (pointerCenter) {
5045
+ const needleH = Math.round(radius * 0.42);
5046
+ pointer.style.cssText = `position: absolute; left: 50%; top: 50%; width: 20px; height: ${needleH}px; transform: ${pointerBase}; transform-origin: 50% 100%; z-index: 6; pointer-events: none;`;
5047
+ if (pointerImg) {
5048
+ const pim = document.createElement("img");
5049
+ pim.src = pointerImg;
5050
+ pim.alt = "";
5051
+ pim.style.cssText = "width: 100%; height: 100%; object-fit: contain; object-position: top; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4));";
5052
+ pointer.appendChild(pim);
5053
+ } else {
5054
+ const stem = document.createElement("div");
5055
+ stem.style.cssText = `position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 4px; height: 100%; background: ${pointerColor}; border-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,0.35);`;
5056
+ const head = document.createElement("div");
5057
+ head.style.cssText = `position: absolute; left: 50%; top: -1px; transform: translateX(-50%); width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 14px solid ${pointerColor}; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.4));`;
5058
+ pointer.appendChild(stem);
5059
+ pointer.appendChild(head);
5060
+ }
5061
+ } else if (pointerImg) {
5062
+ pointer.style.cssText = `position: absolute; left: 50%; top: -12px; transform: ${pointerBase}; transform-origin: 50% 0; width: 30px; height: 34px; z-index: 5;`;
5063
+ const pim = document.createElement("img");
5064
+ pim.src = pointerImg;
5065
+ pim.alt = "";
5066
+ pim.style.cssText = "width: 100%; height: 100%; object-fit: contain; display: block; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));";
5067
+ pointer.appendChild(pim);
5068
+ } else {
5069
+ pointer.style.cssText = `position: absolute; left: 50%; top: -7px; transform: ${pointerBase}; transform-origin: 50% 0; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 16px solid ${pointerColor}; z-index: 5; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));`;
5070
+ }
4699
5071
  const wheel = document.createElement("div");
4700
5072
  const wheelFace = wheelBgImg ? `center / cover no-repeat url("${wheelBgImg}")` : `conic-gradient(${stops})`;
4701
- wheel.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%; border: 5px solid #ffffff; box-shadow: 0 0 0 2px ${text}2e, inset 0 0 22px rgba(0,0,0,0.20); background: ${wheelFace}; overflow: hidden; will-change: transform; touch-action: none; cursor: ${spinMode === "button" ? "default" : "grab"};`;
5073
+ const wheelRim = wheelBgImg ? "0" : "5px solid #ffffff";
5074
+ const wheelShadow = wheelBgImg ? `0 0 0 2px ${text}2e` : `0 0 0 2px ${text}2e, inset 0 0 22px rgba(0,0,0,0.20)`;
5075
+ wheel.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%; border: ${wheelRim}; box-shadow: ${wheelShadow}; background: ${wheelFace}; overflow: hidden; will-change: transform; touch-action: none; cursor: ${spinMode === "button" ? "default" : "grab"};`;
4702
5076
  if (idleAnim === "wobble") wheel.style.animation = "aegisSpinWobble 3s ease-in-out infinite";
5077
+ const sliceImgFrac = Math.max(40, Math.min(100, Number(ic.spin_slice_image_size) || 100)) / 100;
5078
+ if (!wheelBgImg) shown.forEach((s, i) => {
5079
+ const simg = typeof s.image_url === "string" ? s.image_url : "";
5080
+ if (!simg || !isUrl(simg)) return;
5081
+ const pts = ["50% 50%"];
5082
+ const steps = Math.max(2, Math.ceil(spans[i] / 6));
5083
+ for (let st2 = 0; st2 <= steps; st2++) {
5084
+ const a = (starts[i] + spans[i] * st2 / steps) * Math.PI / 180;
5085
+ const x = 50 + 50 * sliceImgFrac * Math.sin(a);
5086
+ const y = 50 - 50 * sliceImgFrac * Math.cos(a);
5087
+ pts.push(`${x.toFixed(2)}% ${y.toFixed(2)}%`);
5088
+ }
5089
+ const segEl = document.createElement("div");
5090
+ segEl.style.cssText = `position: absolute; inset: 0; background: center / cover no-repeat url("${simg}"); clip-path: polygon(${pts.join(",")}); pointer-events: none;`;
5091
+ wheel.appendChild(segEl);
5092
+ });
4703
5093
  if (!wheelBgImg) starts.forEach((b) => {
4704
5094
  const ln = document.createElement("div");
4705
5095
  ln.style.cssText = `position: absolute; left: 50%; top: 0; width: 1.5px; height: 50%; background: rgba(255,255,255,0.55); transform-origin: bottom center; transform: translateX(-50%) rotate(${b}deg); pointer-events: none;`;
@@ -4711,17 +5101,6 @@ const _AegisInAppManager = class _AegisInAppManager {
4711
5101
  const rText = (rHub + rRim) / 2;
4712
5102
  if (!wheelBgImg) labels.forEach((label, i) => {
4713
5103
  const mid = midOf(i);
4714
- const wedgeImg = shown[i] && typeof shown[i].image_url === "string" ? shown[i].image_url : "";
4715
- if (wedgeImg && isUrl(wedgeImg)) {
4716
- const flipImg = mid > 180 && mid < 360;
4717
- const isz = Math.max(18, Math.min(40, bandLen * 0.62));
4718
- const wim = document.createElement("img");
4719
- wim.src = wedgeImg;
4720
- wim.alt = label || "";
4721
- wim.style.cssText = `position: absolute; left: 50%; top: 50%; width: ${isz}px; height: ${isz}px; object-fit: contain; transform: translate(-50%, -50%) rotate(${mid}deg) translateY(-${rText}px) rotate(${flipImg ? 90 : -90}deg); filter: drop-shadow(0 1px 3px rgba(0,0,0,0.55)); pointer-events: none;`;
4722
- wheel.appendChild(wim);
4723
- return;
4724
- }
4725
5104
  const icoVal = shown[i] && typeof shown[i].icon === "string" ? shown[i].icon : "";
4726
5105
  const chord = 2 * rText * Math.sin(spans[i] * Math.PI / 180 / 2);
4727
5106
  const byThickness = chord * 0.6;
@@ -4765,28 +5144,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4765
5144
  const hub = document.createElement("div");
4766
5145
  hub.style.cssText = `position: absolute; left: 50%; top: 50%; width: 40px; height: 40px; transform: translate(-50%, -50%); border-radius: 50%; background: radial-gradient(circle at 35% 30%, #ffffff, #e9edf5); border: 3px solid #fff; box-shadow: 0 3px 8px rgba(0,0,0,0.30); z-index: 4; overflow: hidden; display: flex; align-items: center; justify-content: center;`;
4767
5146
  if (hubImg) {
4768
- const hc = document.createElement("canvas");
4769
- hc.width = 40;
4770
- hc.height = 40;
4771
- hc.style.cssText = "width: 100%; height: 100%;";
4772
- const hcx = hc.getContext("2d");
4773
- const him = new Image();
4774
- him.crossOrigin = "anonymous";
4775
- him.onload = () => {
4776
- if (!hcx) return;
4777
- const ar = him.width / him.height || 1;
4778
- let w = 32, h = 32;
4779
- if (ar > 1) h = 32 / ar;
4780
- else w = 32 * ar;
4781
- hcx.drawImage(him, (40 - w) / 2, (40 - h) / 2, w, h);
4782
- hcx.globalCompositeOperation = "source-in";
4783
- hcx.fillStyle = "#5b626e";
4784
- hcx.fillRect(0, 0, 40, 40);
4785
- };
4786
- him.onerror = () => {
4787
- };
5147
+ hub.style.border = "none";
5148
+ hub.style.background = "transparent";
5149
+ const him = document.createElement("img");
4788
5150
  him.src = hubImg;
4789
- hub.appendChild(hc);
5151
+ him.alt = "";
5152
+ him.style.cssText = "width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block;";
5153
+ hub.appendChild(him);
4790
5154
  } else {
4791
5155
  const hubDot = document.createElement("div");
4792
5156
  hubDot.style.cssText = `width: 11px; height: 11px; border-radius: 50%; background: ${bg};`;
@@ -4800,7 +5164,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4800
5164
  body.appendChild(wheelWrap);
4801
5165
  if (!card.style.position) card.style.position = "relative";
4802
5166
  const result = document.createElement("div");
4803
- result.style.cssText = "font-size: 14px; font-weight: 800; min-height: 20px; text-align: center; letter-spacing: 0.2px;";
5167
+ result.style.cssText = "font-size: 14px; font-weight: 800; min-height: 0; text-align: center; letter-spacing: 0.2px;";
4804
5168
  if (ic.spin_sound_enabled !== false) {
4805
5169
  const soundBtn = document.createElement("button");
4806
5170
  soundBtn.type = "button";
@@ -4815,7 +5179,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4815
5179
  }
4816
5180
  const btn = document.createElement("button");
4817
5181
  btn.textContent = campaign.button_text || "Spin the wheel";
4818
- btn.style.cssText = `padding: 12px 32px; border-radius: 999px; border: none; background: #ffffff; color: ${bg}; font-size: 15px; font-weight: 800; letter-spacing: 0.2px; cursor: pointer; box-shadow: 0 6px 16px rgba(0,0,0,0.22); transition: transform 0.15s, box-shadow 0.15s; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
5182
+ btn.style.cssText = `padding: 9px 22px; border-radius: 999px; border: none; background: ${text}; color: ${bg}; font-size: 13px; font-weight: 700; letter-spacing: 0.2px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.20); transition: transform 0.15s, box-shadow 0.15s; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
4819
5183
  btn.addEventListener("mouseenter", () => {
4820
5184
  if (btn.disabled) return;
4821
5185
  btn.style.transform = "translateY(-1px)";
@@ -4844,12 +5208,12 @@ const _AegisInAppManager = class _AegisInAppManager {
4844
5208
  pVel *= 0.7;
4845
5209
  pDefl += pVel;
4846
5210
  if (Math.abs(pDefl) > 0.06 || Math.abs(pVel) > 0.06) {
4847
- pointer.style.transform = `translateX(-50%) rotate(${pDefl.toFixed(2)}deg)`;
5211
+ pointer.style.transform = `${pointerBase} rotate(${pDefl.toFixed(2)}deg)`;
4848
5212
  pointerRaf = requestAnimationFrame(stepPointer);
4849
5213
  } else {
4850
5214
  pDefl = 0;
4851
5215
  pVel = 0;
4852
- pointer.style.transform = "translateX(-50%) rotate(0deg)";
5216
+ pointer.style.transform = `${pointerBase} rotate(0deg)`;
4853
5217
  pointerRaf = 0;
4854
5218
  }
4855
5219
  };
@@ -5383,6 +5747,34 @@ const _AegisInAppManager = class _AegisInAppManager {
5383
5747
  target.appendChild(card);
5384
5748
  return true;
5385
5749
  }
5750
+ /** Foil palette — a brushed-metal gradient + a contrast "ink" (for the
5751
+ * SCRATCH-HERE text, sparkle motif, and logo tint) derived from the operator's
5752
+ * chosen foil colour (brand primary / custom hex). Unset / invalid → the
5753
+ * default silver. Light foils get dark ink, dark foils get white ink, so the
5754
+ * prompt stays legible on any colour. */
5755
+ _scratchFoilPalette(hex) {
5756
+ const clean = (hex || "").trim().replace(/^#/, "");
5757
+ if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
5758
+ return { g0: "#d7dbe2", g1: "#b4bac4", g2: "#c9cdd6", ink: "#6b7280" };
5759
+ }
5760
+ const r = parseInt(clean.slice(0, 2), 16);
5761
+ const g = parseInt(clean.slice(2, 4), 16);
5762
+ const b = parseInt(clean.slice(4, 6), 16);
5763
+ const toward = (amt) => {
5764
+ const m2 = (c) => Math.round(c + (255 - c) * amt);
5765
+ return `rgb(${m2(r)}, ${m2(g)}, ${m2(b)})`;
5766
+ };
5767
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
5768
+ return {
5769
+ g0: toward(0.34),
5770
+ // bright sheen
5771
+ g1: `#${clean}`,
5772
+ // base colour
5773
+ g2: toward(0.18),
5774
+ // soft sheen
5775
+ ink: lum > 0.62 ? "#5b6270" : "rgba(255,255,255,0.92)"
5776
+ };
5777
+ }
5386
5778
  /** Built-in embedded SCRATCH CARD — a canvas foil the customer scratches to
5387
5779
  * reveal the server-picked prize, then submits (records + grants). Confetti
5388
5780
  * on reveal. No AegisMessageRuntime callback dependency (works on the bill). */
@@ -5400,6 +5792,7 @@ const _AegisInAppManager = class _AegisInAppManager {
5400
5792
  let soundOn = ic.scratch_sound_enabled !== false;
5401
5793
  const revealImg = cfgStr("reveal_image_url");
5402
5794
  const foilImg = cfgStr("scratch_foil_image_url");
5795
+ const foilPal = this._scratchFoilPalette(cfgStr("scratch_foil_color"));
5403
5796
  const pool = Array.isArray(ic.prize_pool) ? ic.prize_pool : [];
5404
5797
  const isUrl = (s) => /^https?:\/\//i.test(s);
5405
5798
  const sampleWin = pool.find(
@@ -5542,9 +5935,9 @@ const _AegisInAppManager = class _AegisInAppManager {
5542
5935
  if (!ctx) return;
5543
5936
  ctx.globalCompositeOperation = "source-over";
5544
5937
  const foil = ctx.createLinearGradient(0, 0, CW, CH);
5545
- foil.addColorStop(0, "#d7dbe2");
5546
- foil.addColorStop(0.5, "#b4bac4");
5547
- foil.addColorStop(1, "#c9cdd6");
5938
+ foil.addColorStop(0, foilPal.g0);
5939
+ foil.addColorStop(0.5, foilPal.g1);
5940
+ foil.addColorStop(1, foilPal.g2);
5548
5941
  ctx.fillStyle = foil;
5549
5942
  ctx.fillRect(0, 0, CW, CH);
5550
5943
  ctx.textAlign = "center";
@@ -5553,17 +5946,19 @@ const _AegisInAppManager = class _AegisInAppManager {
5553
5946
  ctx.globalAlpha = 0.6;
5554
5947
  ctx.drawImage(logo, (CW - logo.width) / 2, CH * 0.42 - logo.height / 2, logo.width, logo.height);
5555
5948
  ctx.globalAlpha = 1;
5556
- ctx.fillStyle = "#6b7280";
5949
+ ctx.fillStyle = foilPal.ink;
5557
5950
  ctx.font = "700 12px system-ui, sans-serif";
5558
5951
  ctx.fillText("SCRATCH HERE", CW / 2, CH * 0.84);
5559
5952
  } else {
5560
- ctx.fillStyle = "rgba(108,116,128,0.18)";
5953
+ ctx.globalAlpha = 0.22;
5954
+ ctx.fillStyle = foilPal.ink;
5561
5955
  ctx.font = "12px system-ui, sans-serif";
5562
5956
  for (let gy = 18; gy < CH; gy += 30) {
5563
5957
  const off = (gy / 30 | 0) % 2 ? 16 : 0;
5564
5958
  for (let gx = 16 + off; gx < CW; gx += 32) ctx.fillText("✦", gx, gy);
5565
5959
  }
5566
- ctx.fillStyle = "#7c828e";
5960
+ ctx.globalAlpha = 1;
5961
+ ctx.fillStyle = foilPal.ink;
5567
5962
  ctx.font = "700 13px system-ui, sans-serif";
5568
5963
  ctx.fillText("SCRATCH HERE", CW / 2, CH / 2);
5569
5964
  }
@@ -5590,7 +5985,7 @@ const _AegisInAppManager = class _AegisInAppManager {
5590
5985
  if (!lctx) return;
5591
5986
  lctx.drawImage(im, 0, 0, lc.width, lc.height);
5592
5987
  lctx.globalCompositeOperation = "source-in";
5593
- lctx.fillStyle = "#5b626e";
5988
+ lctx.fillStyle = foilPal.ink;
5594
5989
  lctx.fillRect(0, 0, lc.width, lc.height);
5595
5990
  paintFoil(lc);
5596
5991
  };
@@ -6315,6 +6710,9 @@ const _AegisInAppManager = class _AegisInAppManager {
6315
6710
  return;
6316
6711
  }
6317
6712
  this.displayedCampaigns.add(campaign.id);
6713
+ this._displaySizeMul = this._sizeMul(
6714
+ campaign.interactive_config
6715
+ );
6318
6716
  const interactiveSubTypes = /* @__PURE__ */ new Set([
6319
6717
  "spin_wheel",
6320
6718
  "scratch_card",
@@ -6507,6 +6905,14 @@ const _AegisInAppManager = class _AegisInAppManager {
6507
6905
  * slot renderers (`renderSpinWheelSlot` / `renderScratchCardSlot`) in a modal
6508
6906
  * so the gamified widget is fully playable — not a dead frame.
6509
6907
  */
6908
+ /** Display-size multiplier — operator control over how big a bounded overlay
6909
+ * renders (small 0.82 / medium 1 / large 1.18). Default medium = prior size.
6910
+ * Read from interactive_config.display_size; mirrors the schema enum + the
6911
+ * dashboard preview harness so the operator's choice is honored identically. */
6912
+ _sizeMul(ic) {
6913
+ const s = ic && typeof ic.display_size === "string" ? ic.display_size : "medium";
6914
+ return s === "small" ? 0.82 : s === "large" ? 1.18 : 1;
6915
+ }
6510
6916
  renderGamificationOverlay(campaign, ic, bg, text) {
6511
6917
  const overlay = this.createOverlay(`aegis-in-app-${campaign.sub_type}-overlay`);
6512
6918
  const modal = document.createElement("div");
@@ -6521,7 +6927,11 @@ const _AegisInAppManager = class _AegisInAppManager {
6521
6927
  this.renderSpinWheelSlot(campaign, ic, bg, text, modal);
6522
6928
  }
6523
6929
  this._addCornerClose(modal, overlay, campaign.id, text);
6524
- overlay.appendChild(modal);
6930
+ const mul = this._sizeMul(ic);
6931
+ const sizer = document.createElement("div");
6932
+ sizer.style.cssText = `display: flex; justify-content: center; align-items: center; width: 100%; transform: scale(${mul}); transform-origin: center;`;
6933
+ sizer.appendChild(modal);
6934
+ overlay.appendChild(sizer);
6525
6935
  this.addAnimationStyles();
6526
6936
  document.body.appendChild(overlay);
6527
6937
  }
@@ -7184,8 +7594,30 @@ const _AegisInAppManager = class _AegisInAppManager {
7184
7594
  background: rgba(0,0,0,0.5); display: flex; align-items: center;
7185
7595
  justify-content: center; z-index: 99999; animation: aegisFadeIn 0.3s ease;
7186
7596
  `;
7597
+ if (this._displaySizeMul !== 1) {
7598
+ const mul = this._displaySizeMul;
7599
+ queueMicrotask(() => {
7600
+ const card = overlay.firstElementChild;
7601
+ if (!card || card.dataset.aegisSized) return;
7602
+ card.dataset.aegisSized = "1";
7603
+ const wrap = document.createElement("div");
7604
+ wrap.style.cssText = `display: flex; align-items: center; justify-content: center; transform: scale(${mul}); transform-origin: center;`;
7605
+ overlay.insertBefore(wrap, card);
7606
+ wrap.appendChild(card);
7607
+ });
7608
+ }
7187
7609
  return overlay;
7188
7610
  }
7611
+ /** Wrap a card in a display-size scaling container (no-op at medium/×1). Used
7612
+ * by formats that build their overlay inline (renderModal) rather than via
7613
+ * createOverlay, so size is honored identically. */
7614
+ _wrapScaled(card) {
7615
+ if (this._displaySizeMul === 1) return card;
7616
+ const wrap = document.createElement("div");
7617
+ wrap.style.cssText = `display: flex; align-items: center; justify-content: center; transform: scale(${this._displaySizeMul}); transform-origin: center;`;
7618
+ wrap.appendChild(card);
7619
+ return wrap;
7620
+ }
7189
7621
  createCTAButton(campaign, bg, text) {
7190
7622
  const btn = document.createElement("button");
7191
7623
  btn.className = "aegis-cta";
@@ -7618,7 +8050,7 @@ const _AegisInAppManager = class _AegisInAppManager {
7618
8050
  actions.appendChild(closeButton);
7619
8051
  content.appendChild(actions);
7620
8052
  modal.appendChild(content);
7621
- overlay.appendChild(modal);
8053
+ overlay.appendChild(this._wrapScaled(modal));
7622
8054
  if (ic.modal_dismiss_on_overlay !== false) {
7623
8055
  overlay.addEventListener("click", (e) => {
7624
8056
  if (e.target === overlay) {
@@ -9054,7 +9486,7 @@ _AegisInAppManager.KNOWN_SURFACES = /* @__PURE__ */ new Set([
9054
9486
  ]);
9055
9487
  _AegisInAppManager.SERVED_DEDUP_PREFIX = "aegis_served_fired:";
9056
9488
  let AegisInAppManager = _AegisInAppManager;
9057
- function renderPreview(config) {
9489
+ function renderPreview(config, opts) {
9058
9490
  document.querySelectorAll(
9059
9491
  '[class^="aegis-in-app-"]'
9060
9492
  ).forEach((el) => {
@@ -9078,12 +9510,33 @@ function renderPreview(config) {
9078
9510
  writeKey: "preview-mode",
9079
9511
  apiHost: "",
9080
9512
  debugMode: false,
9081
- enableSSE: false
9513
+ enableSSE: false,
9514
+ ...(opts == null ? void 0 : opts.cart) ? { getCartState: () => opts.cart } : {}
9082
9515
  });
9083
9516
  const m2 = manager;
9084
9517
  m2.trackEvent = async () => {
9085
9518
  };
9086
9519
  m2.addAnimationStyles();
9520
+ const cfgRec = config;
9521
+ const deliveryModes = Array.isArray(cfgRec.delivery_modes) ? cfgRec.delivery_modes : [];
9522
+ const widgetCategory = typeof cfgRec.widget_category === "string" ? cfgRec.widget_category : "";
9523
+ const inline = deliveryModes.includes("embedded_card") && !!widgetCategory;
9524
+ const coGroup = (opts == null ? void 0 : opts.coGroup) ?? [];
9525
+ if (typeof document !== "undefined" && (coGroup.length > 0 || inline)) {
9526
+ const anchors = document.querySelectorAll("[data-aegis-slot]");
9527
+ if (anchors.length > 0) {
9528
+ try {
9529
+ anchors.forEach((slot) => {
9530
+ slot.querySelectorAll(":scope > :not([data-aegis-slot-default])").forEach((el) => el.remove());
9531
+ });
9532
+ const mm = manager;
9533
+ mm.campaigns = (inline ? [config, ...coGroup] : coGroup).map((c) => ({ ...c, surface: void 0, target_screens: void 0 })).sort((a, b) => (b.priority || 0) - (a.priority || 0));
9534
+ mm.renderIntoSlots();
9535
+ if (inline) return;
9536
+ } catch {
9537
+ }
9538
+ }
9539
+ }
9087
9540
  if (config.type === "stories" || config.sub_type === "stories") {
9088
9541
  const host = document.createElement("div");
9089
9542
  host.className = "aegis-in-app-stories-host";
@@ -12592,10 +13045,13 @@ export {
12592
13045
  TriggerEngine,
12593
13046
  U as UserNamespace,
12594
13047
  bootstrap,
13048
+ closeChat,
12595
13049
  debounce,
12596
13050
  aegis as default,
12597
13051
  deriveDeviceFingerprint,
13052
+ getCurrentLauncher,
12598
13053
  m as murmurhash3_x86_32,
13054
+ openChat,
12599
13055
  readFirstPartyCookie,
12600
13056
  renderPreview,
12601
13057
  throttle,