@sagepilot-ai/web-events-addon 0.2.0 → 0.2.1

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.
@@ -364,7 +364,7 @@ var SagepilotWebEvents = (() => {
364
364
 
365
365
  // package.json
366
366
  var name = "@sagepilot-ai/web-events-addon";
367
- var version = "0.2.0";
367
+ var version = "0.2.1";
368
368
 
369
369
  // src/projection.ts
370
370
  var sensitive = /password|passwd|secret|token|authorization|cookie|session|email|phone|contact|address|card|payment|cvv|ssn|dob|birth|customer|user|account|person|profile|name|description|message|content|text|search|query/i;
@@ -489,7 +489,7 @@ var SagepilotWebEvents = (() => {
489
489
  if (labels.length > 1024) return;
490
490
  for (const label2 of labels) {
491
491
  const value = label2.nextElementSibling;
492
- if (label2.childElementCount || normalized(label2.textContent) !== "product id:" || !value || value.childElementCount || [label2, value].some((node) => node.closest('header,footer,form,dialog,[role="dialog"],[hidden],[aria-hidden="true"],[data-sagepilot-popup],[itemprop="review"],[itemtype$="/Review"],[data-review-id]'))) continue;
492
+ if (label2.childElementCount || normalized(label2.textContent) !== "product id:" || !value || value.childElementCount || [label2, value].some((node2) => node2.closest('header,footer,form,dialog,[role="dialog"],[hidden],[aria-hidden="true"],[data-sagepilot-popup],[itemprop="review"],[itemtype$="/Review"],[data-review-id]'))) continue;
493
493
  const product = id(value.textContent?.trim());
494
494
  if (product) values.push(product);
495
495
  }
@@ -908,7 +908,7 @@ var SagepilotWebEvents = (() => {
908
908
  if (!hydrationLimited) roots.push(...flightRows(chunks.join("")));
909
909
  else roots.length = 0;
910
910
  const title = normalized(document.querySelector("h1")?.textContent), hydrated = [], typedVariants = /* @__PURE__ */ new Map();
911
- const anchors = [...document.querySelectorAll("a[href]")].slice(0, 512).map((node) => publicURL(node.getAttribute("href"))).filter((value) => !!value);
911
+ const anchors = [...document.querySelectorAll("a[href]")].slice(0, 512).map((node2) => publicURL(node2.getAttribute("href"))).filter((value) => !!value);
912
912
  const pending = roots.map((value) => ({ value, product: false, depth: 0 }));
913
913
  let visited = 0;
914
914
  while (pending.length && visited++ < 8192) {
@@ -1079,7 +1079,7 @@ var SagepilotWebEvents = (() => {
1079
1079
  const action = commerceAction(root.getAttribute("aria-label") ?? root.innerText ?? root.textContent ?? "");
1080
1080
  const primary = /^h1$/i.test(root.tagName) || root.matches("button,[role='button']") && /^(?:add(?: to (?:cart|bag|basket))?|buy(?: now)?|rent(?: now)?)[.!]?$/i.test(action);
1081
1081
  if (!anchor && primary && !samePage) return empty("unresolved");
1082
- const form = root.form ?? root.closest("form"), privateForm = form && [...form.querySelectorAll("input:not([type='hidden']),textarea,select")].some((node) => node.tagName === "TEXTAREA" || !/^(?:quantity|qty|variant(?:_id)?|option(?:\[.*\])?|attribute_[A-Za-z0-9_]+)$/i.test(node.getAttribute("name") ?? ""));
1082
+ const form = root.form ?? root.closest("form"), privateForm = form && [...form.querySelectorAll("input:not([type='hidden']),textarea,select")].some((node2) => node2.tagName === "TEXTAREA" || !/^(?:quantity|qty|variant(?:_id)?|option(?:\[.*\])?|attribute_[A-Za-z0-9_]+)$/i.test(node2.getAttribute("name") ?? ""));
1083
1083
  if (privateForm) return empty("unresolved");
1084
1084
  let pageContext = !anchor && primary && samePage && !privateForm && !root.closest("dialog,[role='dialog']");
1085
1085
  const images = /* @__PURE__ */ new Set(), urls = /* @__PURE__ */ new Set(), skus = /* @__PURE__ */ new Set(), variants = /* @__PURE__ */ new Set(), ids = /* @__PURE__ */ new Set();
@@ -1093,7 +1093,7 @@ var SagepilotWebEvents = (() => {
1093
1093
  if (primary && own && !forms.includes(own)) return empty("unresolved");
1094
1094
  const checked = [...document.querySelectorAll('input[type="radio"]:checked')];
1095
1095
  if (checked.length > 32) return empty("ambiguous");
1096
- const controls = checked.map((node) => id(node.value)).filter((value) => value && bound.some((entry) => entry.variant === value));
1096
+ const controls = checked.map((node2) => id(node2.value)).filter((value) => value && bound.some((entry) => entry.variant === value));
1097
1097
  const selections = [.../* @__PURE__ */ new Set([...(own ? forms.filter((value) => value === own) : forms).map((form2) => form2.variant), ...controls])];
1098
1098
  const nativeConflict = selections.length && (selections.length !== 1 || currentSelector && !sameVariant(selections[0], currentSelector));
1099
1099
  const nativeVariant = samePage && selections.length === 1 && (!currentSelector || sameVariant(selections[0], currentSelector)) ? selections[0] : void 0;
@@ -1107,20 +1107,20 @@ var SagepilotWebEvents = (() => {
1107
1107
  while ((next = walker.nextNode()) && nodes2.length <= 64) nodes2.push(next);
1108
1108
  if (nodes2.length > 64) break;
1109
1109
  if (!direct && card.matches("[data-product-id],[data-variant-id],[data-product_id],[data-variant_id]")) direct = card;
1110
- for (const node of nodes2) {
1111
- const image = publicURL(node.getAttribute("src") ?? void 0, location.href, true);
1110
+ for (const node2 of nodes2) {
1111
+ const image = publicURL(node2.getAttribute("src") ?? void 0, location.href, true);
1112
1112
  if (image) images.add(image);
1113
- for (const match of String(getComputedStyle(node).backgroundImage ?? "").matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
1113
+ for (const match of String(getComputedStyle(node2).backgroundImage ?? "").matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
1114
1114
  const value = publicURL(match[1], location.href, true);
1115
1115
  if (value) images.add(value);
1116
1116
  }
1117
- const url = publicURL(node.getAttribute("href") ?? void 0);
1117
+ const url = publicURL(node2.getAttribute("href") ?? void 0);
1118
1118
  if (url) urls.add(url);
1119
- const sku = id(node.getAttribute("data-sku") ?? (node.getAttribute("itemprop") === "sku" ? node.getAttribute("content") : null));
1119
+ const sku = id(node2.getAttribute("data-sku") ?? (node2.getAttribute("itemprop") === "sku" ? node2.getAttribute("content") : null));
1120
1120
  if (sku) skus.add(sku);
1121
- const variant = id(node.getAttribute("data-variant-id") ?? node.getAttribute("data-variant_id"));
1121
+ const variant = id(node2.getAttribute("data-variant-id") ?? node2.getAttribute("data-variant_id"));
1122
1122
  if (variant) variants.add(variant);
1123
- const product = id(node.getAttribute("data-product-id") ?? node.getAttribute("data-product_id") ?? (node.getAttribute("itemprop") === "productID" ? node.getAttribute("content") : null));
1123
+ const product = id(node2.getAttribute("data-product-id") ?? node2.getAttribute("data-product_id") ?? (node2.getAttribute("itemprop") === "productID" ? node2.getAttribute("content") : null));
1124
1124
  if (product) ids.add(product);
1125
1125
  }
1126
1126
  if (targetURL || images.size && urls.size) break;
@@ -1159,8 +1159,8 @@ var SagepilotWebEvents = (() => {
1159
1159
  const exactURL = !!desired && (entry.url === desired || !!selected && entry.url?.split("?")[0] === desired.split("?")[0] && [entry.variant, entry.sku, ...variantValues].some((value) => sameVariant(value, selected)));
1160
1160
  const image = entry.images.find((value) => images.has(value) && !sharedImages.has(value)), token = entry.sku && new RegExp(`(?:^|[^A-Za-z0-9])${entry.sku.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:$|[^A-Za-z0-9])`);
1161
1161
  const skuImage = token ? [...images].find((value) => !sharedImages.has(value) && token.test(new URL(value).pathname)) : void 0;
1162
- const title = normalized(entry.snapshot.name), titleOnPage = [...document.querySelectorAll("h1")].slice(0, 8).some((node) => node.getClientRects().length && normalized(node.textContent) === title);
1163
- const groupOnPage = entry.group?.url === canonical && samePage && entry.snapshot.metadata.provenance.source_ref === current && [...document.querySelectorAll("h1")].slice(0, 8).some((node) => node.getClientRects().length && normalized(node.textContent) === entry.group?.name);
1162
+ const title = normalized(entry.snapshot.name), titleOnPage = [...document.querySelectorAll("h1")].slice(0, 8).some((node2) => node2.getClientRects().length && normalized(node2.textContent) === title);
1163
+ const groupOnPage = entry.group?.url === canonical && samePage && entry.snapshot.metadata.provenance.source_ref === current && [...document.querySelectorAll("h1")].slice(0, 8).some((node2) => node2.getClientRects().length && normalized(node2.textContent) === entry.group?.name);
1164
1164
  const directImage = uniqueLiteral && !targetURL && samePage && pageSchema.includes(entry) && entry.rawImages?.includes(literalImage) && !sharedImages.has(clickedImage) && (!entry.url || entry.url.split("?")[0] === canonical.split("?")[0]) && (!currentSelector || [entry.variant, entry.sku, declaredVariantURL, ...variantValues].includes(currentSelector));
1165
1165
  const corroborated = title && title === normalized(observedLabel) && observedPrice !== void 0 && entry.snapshot.price === observedPrice;
1166
1166
  let match = exactURL ? "exact_url" : !targetURL && (entry.variant && variants.has(entry.variant) || exactVariant && sameVariant(entry.variant ?? declaredVariantURL, exactVariant)) ? "exact_variant" : labelMatch || !targetURL && typeof entry.snapshot.product_id === "string" && ids.has(entry.snapshot.product_id) ? "direct_id" : !targetURL && entry.sku && skus.has(entry.sku) ? "exact_sku" : skuImage && (!targetURL || !entry.url) ? "exact_sku" : directImage || image && (corroborated || entry.blocked || entry.url === current && titleOnPage || groupOnPage) && (!targetURL || !entry.url) ? "exact_image" : null;
@@ -1264,13 +1264,13 @@ var SagepilotWebEvents = (() => {
1264
1264
  if (candidate.closest(privateRegion)) return null;
1265
1265
  const children = nodes(candidate, 64);
1266
1266
  if (!children) return null;
1267
- if (children.some((node) => node.matches("input,textarea,select,[contenteditable='true']"))) return null;
1268
- const images = children.filter((node) => node.matches(imageSelector));
1267
+ if (children.some((node2) => node2.matches("input,textarea,select,[contenteditable='true']"))) return null;
1268
+ const images = children.filter((node2) => node2.matches(imageSelector));
1269
1269
  if (images.length > 1) return null;
1270
1270
  if (images.length !== 1) continue;
1271
1271
  const name2 = label(images[0].getAttribute("alt") ?? images[0].getAttribute("aria-label"));
1272
- if (!name2 || !children.some((node) => node !== images[0] && !node.childElementCount && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden" && label(text(node)) === name2)) continue;
1273
- const amounts = children.filter((node) => node.matches("p,span,[itemprop='price']") && !node.closest("del,s") && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden").map((node) => price(text(node))).filter(Boolean);
1272
+ if (!name2 || !children.some((node2) => node2 !== images[0] && !node2.childElementCount && node2.getClientRects().length && getComputedStyle(node2).visibility !== "hidden" && label(text(node2)) === name2)) continue;
1273
+ const amounts = children.filter((node2) => node2.matches("p,span,[itemprop='price']") && !node2.closest("del,s") && node2.getClientRects().length && getComputedStyle(node2).visibility !== "hidden").map((node2) => price(text(node2))).filter(Boolean);
1274
1274
  if (!amounts.length || new Set(amounts.map((amount) => `${amount.price}:${amount.currency ?? ""}`)).size > 1) continue;
1275
1275
  const siblings = candidate.parentElement?.children;
1276
1276
  if (siblings && siblings.length > 64) return null;
@@ -1278,14 +1278,14 @@ var SagepilotWebEvents = (() => {
1278
1278
  if (sibling === candidate) continue;
1279
1279
  const entries = nodes(sibling, 64);
1280
1280
  if (!entries) return null;
1281
- if (entries.some((node) => node.matches(imageSelector) && label(node.getAttribute("alt") ?? node.getAttribute("aria-label")) === name2)) return null;
1281
+ if (entries.some((node2) => node2.matches(imageSelector) && label(node2.getAttribute("alt") ?? node2.getAttribute("aria-label")) === name2)) return null;
1282
1282
  }
1283
1283
  return { node: candidate, label: name2, ...amounts[0] };
1284
1284
  }
1285
1285
  return null;
1286
1286
  }
1287
1287
  function quantity(product) {
1288
- const values = (nodes(product.node, 64) ?? []).filter((node) => (!node.childElementCount || node.matches("p")) && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden").map((node) => text(node)?.match(/^(?:.*\s)?(?:quantity|qty)\s*:\s*(\d{1,6})$/i)?.[1]).filter((value) => value !== void 0);
1288
+ const values = (nodes(product.node, 64) ?? []).filter((node2) => (!node2.childElementCount || node2.matches("p")) && node2.getClientRects().length && getComputedStyle(node2).visibility !== "hidden").map((node2) => text(node2)?.match(/^(?:.*\s)?(?:quantity|qty)\s*:\s*(\d{1,6})$/i)?.[1]).filter((value) => value !== void 0);
1289
1289
  const unique = [...new Set(values)];
1290
1290
  return unique.length === 1 ? Number(unique[0]) : null;
1291
1291
  }
@@ -1295,43 +1295,43 @@ var SagepilotWebEvents = (() => {
1295
1295
  function cart(wanted) {
1296
1296
  const markers = [], possible = document.querySelectorAll("h1,h2,h3,h4,h5,h6,span,[aria-label],aside,[role='dialog']");
1297
1297
  for (let index = 0; index < Math.min(possible.length, 128); index++) {
1298
- const node2 = possible[index];
1299
- if (!node2.childElementCount && /^(?:shopping )?(?:cart|bag)$/i.test(text(node2) ?? "")) markers.push(node2);
1298
+ const node3 = possible[index];
1299
+ if (!node3.childElementCount && /^(?:shopping )?(?:cart|bag)$/i.test(text(node3) ?? "")) markers.push(node3);
1300
1300
  }
1301
1301
  const candidates = /* @__PURE__ */ new Set();
1302
1302
  for (const marker of markers.slice(0, 8)) {
1303
- let node2 = marker.parentElement;
1304
- for (let depth = 0; node2 && node2 !== document.body && depth < 6; depth++, node2 = node2.parentElement) {
1305
- const children = nodes(node2, 256);
1303
+ let node3 = marker.parentElement;
1304
+ for (let depth = 0; node3 && node3 !== document.body && depth < 6; depth++, node3 = node3.parentElement) {
1305
+ const children = nodes(node3, 256);
1306
1306
  if (!children) break;
1307
1307
  if (children.some((child) => child.getClientRects().length && getComputedStyle(child).visibility !== "hidden" && (child.matches("[aria-busy='true'],[role='progressbar']") || !child.childElementCount && /^(?:loading|updating)[.\s…]*$/i.test(text(child) ?? "")))) break;
1308
1308
  const buttons = children.filter((child) => child.matches("button,[role='button']"));
1309
1309
  if (!buttons.some((button) => /^(?:proceed to )?checkout$/i.test(text(button) ?? ""))) continue;
1310
- if (buttons.some((button) => /\badd\b.*\bcart\b/i.test(text(button) ?? "")) || node2.closest("[hidden],[aria-hidden='true'],[aria-busy='true']")) break;
1311
- const style = getComputedStyle(node2);
1310
+ if (buttons.some((button) => /\badd\b.*\bcart\b/i.test(text(button) ?? "")) || node3.closest("[hidden],[aria-hidden='true'],[aria-busy='true']")) break;
1311
+ const style = getComputedStyle(node3);
1312
1312
  if (style.display === "none" || style.visibility === "hidden") break;
1313
- candidates.add(node2);
1313
+ candidates.add(node3);
1314
1314
  break;
1315
1315
  }
1316
1316
  }
1317
1317
  if (candidates.size !== 1) return null;
1318
- const node = [...candidates][0], images = (nodes(node, 256) ?? []).filter((child) => child.matches(imageSelector));
1318
+ const node2 = [...candidates][0], images = (nodes(node2, 256) ?? []).filter((child) => child.matches(imageSelector));
1319
1319
  if (images.length > 16) return null;
1320
- if (!images.length && !(nodes(node, 256) ?? []).some((child) => /^(?:(?:your|the|shopping)\s+)?(?:cart|bag)\s+(?:is\s+)?empty[.!:]?$|^add some products (?:in|to) (?:the|your) (?:cart|bag)\b/i.test(text(child) ?? ""))) return null;
1320
+ if (!images.length && !(nodes(node2, 256) ?? []).some((child) => /^(?:(?:your|the|shopping)\s+)?(?:cart|bag)\s+(?:is\s+)?empty[.!:]?$|^add some products (?:in|to) (?:the|your) (?:cart|bag)\b/i.test(text(child) ?? ""))) return null;
1321
1321
  const seen = /* @__PURE__ */ new Set();
1322
1322
  let count = 0;
1323
1323
  for (const image of images) {
1324
1324
  const product = productAt(image), amount = product ? quantity(product) : null;
1325
- if (!product || !node.contains(product.node) || amount === null || seen.has(product.label)) return null;
1325
+ if (!product || !node2.contains(product.node) || amount === null || seen.has(product.label)) return null;
1326
1326
  seen.add(product.label);
1327
1327
  if (product.label === wanted) count = amount;
1328
1328
  }
1329
- let ref = regions.get(node);
1329
+ let ref = regions.get(node2);
1330
1330
  if (!ref) {
1331
1331
  ref = crypto.randomUUID();
1332
- regions.set(node, ref);
1332
+ regions.set(node2, ref);
1333
1333
  }
1334
- return { node, ref, quantity: count };
1334
+ return { node: node2, ref, quantity: count };
1335
1335
  }
1336
1336
  function remove(key) {
1337
1337
  const item = pending.get(key);
@@ -1466,7 +1466,7 @@ var SagepilotWebEvents = (() => {
1466
1466
  }
1467
1467
  function click(event) {
1468
1468
  const epoch = options.epoch();
1469
- if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((node) => node instanceof Element && node.hasAttribute("data-sagepilot-popup"))) return;
1469
+ if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((node2) => node2 instanceof Element && node2.hasAttribute("data-sagepilot-popup"))) return;
1470
1470
  if (pendingClicks >= 128) {
1471
1471
  options.dropped();
1472
1472
  return;
@@ -1474,12 +1474,12 @@ var SagepilotWebEvents = (() => {
1474
1474
  try {
1475
1475
  const target = event.target.closest("button,a,input,select,textarea,[role='button']") ?? event.target;
1476
1476
  const path = [];
1477
- let node = target;
1478
- for (let depth = 0; node && depth < 6; depth++, node = node.parentElement) {
1479
- const role = node.getAttribute("role"), type = node.getAttribute("type"), parent = node.parentElement;
1480
- const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children, node) : -1;
1477
+ let node2 = target;
1478
+ for (let depth = 0; node2 && depth < 6; depth++, node2 = node2.parentElement) {
1479
+ const role = node2.getAttribute("role"), type = node2.getAttribute("type"), parent = node2.parentElement;
1480
+ const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children, node2) : -1;
1481
1481
  path.push({
1482
- tag: node.tagName.toLowerCase(),
1482
+ tag: node2.tagName.toLowerCase(),
1483
1483
  sibling_index: sibling,
1484
1484
  ...role && roles.has(role) ? { role } : {},
1485
1485
  ...type && /^(button|submit|reset|checkbox|radio)$/.test(type) ? { type } : {}
@@ -2053,6 +2053,153 @@ var SagepilotWebEvents = (() => {
2053
2053
  };
2054
2054
  }
2055
2055
 
2056
+ // src/popup-design.ts
2057
+ var layouts = ["centered", "image_left", "image_right", "bottom_sheet"];
2058
+ var builtIns = /* @__PURE__ */ new Set(["logo", "title", "body", "hero", "form"]);
2059
+ var defaultBlocks = [{ kind: "logo" }, { kind: "hero" }, { kind: "title" }, { kind: "body" }, { kind: "form" }];
2060
+ function asset(value) {
2061
+ if (typeof value !== "string" || value.length > 1e3 || !value.startsWith("https://") || /[\s\\'"<>]/.test(value)) return false;
2062
+ try {
2063
+ const url = new URL(value);
2064
+ return url.protocol === "https:" && !!url.hostname && url.port !== "0" && !url.username && !url.password && !url.hash;
2065
+ } catch {
2066
+ return false;
2067
+ }
2068
+ }
2069
+ function validPopupDesign(design) {
2070
+ if (design == null) return true;
2071
+ const color = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
2072
+ const integer = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
2073
+ if (!layouts.includes(design.layout) || ![design.background_color, design.text_color, design.button_text_color].every(color) || !integer(design.border_radius, 0, 40) || !integer(design.padding, 12, 40) || !integer(design.width, 280, 760) || design.font_family !== null && (typeof design.font_family !== "string" || !/^[A-Za-z0-9][A-Za-z0-9 _-]{0,79}$/.test(design.font_family)) || [design.font_url, design.image_url].some((url) => url !== null && !asset(url)) || typeof design.image_alt !== "string" || design.image_alt.length > 200) return false;
2074
+ if (design.blocks === null) return true;
2075
+ if (!Array.isArray(design.blocks) || design.blocks.length > 12) return false;
2076
+ const seen = /* @__PURE__ */ new Set();
2077
+ return design.blocks.every((block) => {
2078
+ if (!block || typeof block !== "object") return false;
2079
+ if (builtIns.has(block.kind)) {
2080
+ if (seen.has(block.kind)) return false;
2081
+ seen.add(block.kind);
2082
+ return true;
2083
+ }
2084
+ if (block.kind === "heading" || block.kind === "text") return typeof block.text === "string" && block.text.length > 0 && block.text.length <= 600;
2085
+ if (block.kind === "image") return asset(block.url) && typeof block.alt === "string" && block.alt.length <= 200;
2086
+ if (block.kind === "link") return asset(block.url) && typeof block.text === "string" && block.text.length > 0 && block.text.length <= 120;
2087
+ return false;
2088
+ }) && seen.has("form");
2089
+ }
2090
+ function node(tag, value) {
2091
+ const result = document.createElement(tag);
2092
+ if (value !== void 0) result.textContent = value;
2093
+ return result;
2094
+ }
2095
+ function renderPopupDesign(panel, host, config, form) {
2096
+ const design = config.design;
2097
+ const blocks = design?.blocks ?? defaultBlocks;
2098
+ const content = node("div");
2099
+ content.className = "content";
2100
+ const hero = design?.image_url && blocks.some((block) => block.kind === "hero") ? popupImage(design.image_url, design.image_alt, "hero") : null;
2101
+ const split = hero && (design?.layout === "image_left" || design?.layout === "image_right");
2102
+ if (split) {
2103
+ const side = node("div");
2104
+ side.className = "side-art";
2105
+ side.append(hero);
2106
+ panel.append(side);
2107
+ }
2108
+ let heading = false;
2109
+ for (const block of blocks) {
2110
+ switch (block.kind) {
2111
+ case "logo":
2112
+ if (config.logo_url) content.append(popupImage(config.logo_url, "", "logo"));
2113
+ break;
2114
+ case "hero":
2115
+ if (hero && !split) content.append(hero);
2116
+ break;
2117
+ case "title":
2118
+ case "heading": {
2119
+ const title = node(heading ? "h3" : "h2", block.kind === "heading" ? block.text : config.title);
2120
+ if (!heading) {
2121
+ title.id = "sage-popup-title";
2122
+ heading = true;
2123
+ }
2124
+ content.append(title);
2125
+ break;
2126
+ }
2127
+ case "body":
2128
+ content.append(node("p", config.body));
2129
+ break;
2130
+ case "text":
2131
+ content.append(node("p", block.text));
2132
+ break;
2133
+ case "image":
2134
+ content.append(popupImage(block.url, block.alt, "block-image"));
2135
+ break;
2136
+ case "link": {
2137
+ const link = node("a", block.text);
2138
+ link.href = block.url;
2139
+ link.target = "_blank";
2140
+ link.rel = "noopener noreferrer";
2141
+ content.append(link);
2142
+ break;
2143
+ }
2144
+ case "form":
2145
+ content.append(form);
2146
+ break;
2147
+ }
2148
+ }
2149
+ panel.append(content);
2150
+ if (heading) panel.setAttribute("aria-labelledby", "sage-popup-title");
2151
+ else panel.setAttribute("aria-label", config.title);
2152
+ if (!design) return () => {
2153
+ };
2154
+ panel.dataset.design = "";
2155
+ panel.dataset.layout = split ? design.layout : design.layout === "bottom_sheet" ? "bottom_sheet" : "centered";
2156
+ if (panel.dataset.layout === "bottom_sheet") {
2157
+ host.style.alignItems = "end";
2158
+ host.style.paddingBottom = "0";
2159
+ }
2160
+ for (const [key, value] of Object.entries({
2161
+ "--popup-surface": design.background_color,
2162
+ "--popup-text": design.text_color,
2163
+ "--popup-button-text": design.button_text_color,
2164
+ "--popup-radius": design.border_radius + "px",
2165
+ "--popup-padding": design.padding + "px",
2166
+ "--popup-width": design.width + "px"
2167
+ })) panel.style.setProperty(key, value);
2168
+ let face;
2169
+ const fallback = design.font_family ? JSON.stringify(design.font_family) + ", system-ui, sans-serif" : "system-ui, sans-serif";
2170
+ if (design.font_url && typeof FontFace === "function" && document.fonts) {
2171
+ try {
2172
+ face = new FontFace("sage-popup-" + crypto.randomUUID(), "url(" + JSON.stringify(design.font_url) + ")");
2173
+ document.fonts.add(face);
2174
+ panel.style.fontFamily = JSON.stringify(face.family) + ", " + fallback;
2175
+ } catch {
2176
+ panel.style.fontFamily = fallback;
2177
+ }
2178
+ } else panel.style.fontFamily = fallback;
2179
+ return () => {
2180
+ if (face) document.fonts.delete(face);
2181
+ };
2182
+ }
2183
+ function popupImage(url, alt, className) {
2184
+ const image = node("img");
2185
+ image.referrerPolicy = "no-referrer";
2186
+ image.alt = alt;
2187
+ image.className = className;
2188
+ image.src = url;
2189
+ return image;
2190
+ }
2191
+ var popupDesignStyles = [
2192
+ ".panel[data-design]{background:var(--popup-surface);color:var(--popup-text);border-radius:var(--popup-radius);width:min(var(--popup-width),calc(100vw - 40px));padding:0;overflow-wrap:anywhere}",
2193
+ ".panel[data-design] .content{min-width:0;padding:var(--popup-padding)}",
2194
+ ".panel[data-design] :is(p,a){color:inherit;white-space:pre-wrap}",
2195
+ ".panel[data-design] input:not([type=checkbox]){color:inherit;background:transparent;border-color:currentColor}",
2196
+ ".panel[data-design] button[type=submit]{color:var(--popup-button-text)}",
2197
+ ".panel[data-design] :is(.hero,.block-image){display:block;max-width:100%;width:100%;max-height:240px;object-fit:cover;border-radius:calc(var(--popup-radius)/2)}",
2198
+ ".panel[data-layout=bottom_sheet]{border-end-start-radius:0;border-end-end-radius:0}",
2199
+ ".panel .side-art{position:relative;min-width:0;overflow:hidden}.panel .side-art>.hero{border-radius:0}.panel[data-design] .close{z-index:1}",
2200
+ "@media(min-width:600px){.panel:is([data-layout=image_left],[data-layout=image_right]){display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);align-items:stretch}.panel:is([data-layout=image_left],[data-layout=image_right]) .side-art>.hero{position:absolute;inset:0;height:100%;max-height:none}.panel[data-layout=image_right]{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}.panel[data-layout=image_right]>.side-art{grid-row:1;grid-column:2}.panel[data-layout=image_right]>.content{grid-row:1;grid-column:1}}"
2201
+ ].join("");
2202
+
2056
2203
  // src/popup.ts
2057
2204
  function popupCompletionKey(workspaceId, installationId) {
2058
2205
  return `sagepilot:sdk-popup:${workspaceId}:${installationId}:completed`;
@@ -2067,7 +2214,7 @@ var SagepilotWebEvents = (() => {
2067
2214
  return false;
2068
2215
  }
2069
2216
  }
2070
- return true;
2217
+ return validPopupDesign(value.design);
2071
2218
  }
2072
2219
  function createPopup(options) {
2073
2220
  let config = null, host = null;
@@ -2100,9 +2247,9 @@ var SagepilotWebEvents = (() => {
2100
2247
  return !raw || !Number.isFinite(last) || Date.now() - last >= config.frequency_hours * 36e5;
2101
2248
  }
2102
2249
  function element(tag, text2) {
2103
- const node = document.createElement(tag);
2104
- if (text2 !== void 0) node.textContent = text2;
2105
- return node;
2250
+ const node2 = document.createElement(tag);
2251
+ if (text2 !== void 0) node2.textContent = text2;
2252
+ return node2;
2106
2253
  }
2107
2254
  function show() {
2108
2255
  if (!eligible() || !config) return;
@@ -2114,12 +2261,12 @@ var SagepilotWebEvents = (() => {
2114
2261
  const shadow = host.attachShadow({ mode: "open" });
2115
2262
  const style = element("style");
2116
2263
  style.textContent = `:host{font-family:system-ui,sans-serif;color:#172c23;color-scheme:light}*{box-sizing:border-box}.panel{background:white;width:min(420px,calc(100vw - 40px));max-height:90dvh;overflow:auto;border-radius:18px;padding:28px;box-shadow:0 16px 80px #0003;position:relative}h2{margin:4px 28px 10px 0;font-size:25px}p{line-height:1.5;color:#52645a}label{display:block;margin-top:14px;font-size:14px}input{display:block;width:100%;padding:11px;border:1px solid #b6c7bd;border-radius:7px;margin-top:6px;font:inherit;background:white;color:#172c23}button{cursor:pointer;font:inherit;color:inherit}button[type=submit]{display:block;width:100%;border:0;border-radius:8px;background:${published.accent_color};color:white;padding:13px;margin-top:20px;font-weight:600}button:disabled{opacity:.6;cursor:wait}.close{position:absolute;right:15px;top:12px;border:0;background:transparent;font-size:25px}.status{min-height:1.3em;font-size:14px}.logo{max-width:160px;max-height:60px;object-fit:contain}input:focus-visible,button:focus-visible{outline:3px solid #729e82;outline-offset:3px}@media(max-width:480px){.panel{padding:22px}h2{font-size:22px}}`;
2264
+ style.textContent += popupDesignStyles;
2117
2265
  shadow.append(style);
2118
2266
  const panel = element("section");
2119
2267
  panel.className = "panel";
2120
2268
  panel.setAttribute("role", "dialog");
2121
2269
  panel.setAttribute("aria-modal", "true");
2122
- panel.setAttribute("aria-labelledby", "sage-popup-title");
2123
2270
  const close = element("button", "\xD7");
2124
2271
  close.type = "button";
2125
2272
  close.className = "close";
@@ -2130,17 +2277,6 @@ var SagepilotWebEvents = (() => {
2130
2277
  }
2131
2278
  close.addEventListener("click", dismiss);
2132
2279
  panel.append(close);
2133
- if (published.logo_url) {
2134
- const logo = element("img");
2135
- logo.src = published.logo_url;
2136
- logo.alt = "";
2137
- logo.className = "logo";
2138
- logo.referrerPolicy = "no-referrer";
2139
- panel.append(logo);
2140
- }
2141
- const title = element("h2", published.title);
2142
- title.id = "sage-popup-title";
2143
- panel.append(title, element("p", published.body));
2144
2280
  const form = element("form"), status = element("p");
2145
2281
  status.className = "status";
2146
2282
  status.setAttribute("role", "status");
@@ -2164,7 +2300,7 @@ var SagepilotWebEvents = (() => {
2164
2300
  const submit = element("button", published.button_text);
2165
2301
  submit.type = "submit";
2166
2302
  form.append(submit, status);
2167
- panel.append(form);
2303
+ const cleanupDesign = renderPopupDesign(panel, host, published, form);
2168
2304
  shadow.append(panel);
2169
2305
  document.body.append(host);
2170
2306
  options.storage.setItem(shownKey, String(Date.now()));
@@ -2181,8 +2317,8 @@ var SagepilotWebEvents = (() => {
2181
2317
  if (challenge) {
2182
2318
  await options.identify(challenge, inputs.get("code").value);
2183
2319
  if (revision !== generation) return;
2184
- form.replaceChildren(element("p", published.success_text), close);
2185
- close.className = "close";
2320
+ form.replaceChildren(element("p", published.success_text));
2321
+ close.focus();
2186
2322
  } else {
2187
2323
  const contact = { submission_id: submissionId, popup_revision: published.revision, marketing_consent: true };
2188
2324
  for (const field of ["name", "email", "phone"]) {
@@ -2224,7 +2360,10 @@ var SagepilotWebEvents = (() => {
2224
2360
  form.replaceChildren(label2, submit, restart, status);
2225
2361
  status.textContent = "Check your email for the six-digit code.";
2226
2362
  code.focus();
2227
- } else form.replaceChildren(element("p", published.success_text));
2363
+ } else {
2364
+ form.replaceChildren(element("p", published.success_text));
2365
+ close.focus();
2366
+ }
2228
2367
  }
2229
2368
  } catch (error) {
2230
2369
  if (revision === generation) status.textContent = challenge ? "Verification failed. Check the code and try again." : "We couldn't save the form. Please try again.";
@@ -2242,7 +2381,7 @@ var SagepilotWebEvents = (() => {
2242
2381
  return;
2243
2382
  }
2244
2383
  if (event.key !== "Tab") return;
2245
- const focusable = [...shadow.querySelectorAll("button:not(:disabled),input:not(:disabled)")];
2384
+ const focusable = [...shadow.querySelectorAll("button:not(:disabled),input:not(:disabled),a[href]")];
2246
2385
  const first = focusable[0], last = focusable[focusable.length - 1];
2247
2386
  if (event.shiftKey && shadow.activeElement === first) {
2248
2387
  event.preventDefault();
@@ -2254,6 +2393,7 @@ var SagepilotWebEvents = (() => {
2254
2393
  }
2255
2394
  shadow.addEventListener("keydown", keydown);
2256
2395
  cleanup = () => {
2396
+ cleanupDesign();
2257
2397
  form.removeEventListener("submit", submitForm);
2258
2398
  shadow.removeEventListener("keydown", keydown);
2259
2399
  };