@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.
package/dist/index.js CHANGED
@@ -338,7 +338,7 @@ function createSagepilotInstallationClient(options) {
338
338
 
339
339
  // package.json
340
340
  var name = "@sagepilot-ai/web-events-addon";
341
- var version = "0.2.0";
341
+ var version = "0.2.1";
342
342
 
343
343
  // src/projection.ts
344
344
  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;
@@ -463,7 +463,7 @@ function labeledProductID() {
463
463
  if (labels.length > 1024) return;
464
464
  for (const label2 of labels) {
465
465
  const value = label2.nextElementSibling;
466
- 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;
466
+ 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;
467
467
  const product = id(value.textContent?.trim());
468
468
  if (product) values.push(product);
469
469
  }
@@ -882,7 +882,7 @@ function createProductIndex(options) {
882
882
  if (!hydrationLimited) roots.push(...flightRows(chunks.join("")));
883
883
  else roots.length = 0;
884
884
  const title = normalized(document.querySelector("h1")?.textContent), hydrated = [], typedVariants = /* @__PURE__ */ new Map();
885
- const anchors = [...document.querySelectorAll("a[href]")].slice(0, 512).map((node) => publicURL(node.getAttribute("href"))).filter((value) => !!value);
885
+ const anchors = [...document.querySelectorAll("a[href]")].slice(0, 512).map((node2) => publicURL(node2.getAttribute("href"))).filter((value) => !!value);
886
886
  const pending = roots.map((value) => ({ value, product: false, depth: 0 }));
887
887
  let visited = 0;
888
888
  while (pending.length && visited++ < 8192) {
@@ -1053,7 +1053,7 @@ function createProductIndex(options) {
1053
1053
  const action = commerceAction(root.getAttribute("aria-label") ?? root.innerText ?? root.textContent ?? "");
1054
1054
  const primary = /^h1$/i.test(root.tagName) || root.matches("button,[role='button']") && /^(?:add(?: to (?:cart|bag|basket))?|buy(?: now)?|rent(?: now)?)[.!]?$/i.test(action);
1055
1055
  if (!anchor && primary && !samePage) return empty("unresolved");
1056
- 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") ?? ""));
1056
+ 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") ?? ""));
1057
1057
  if (privateForm) return empty("unresolved");
1058
1058
  let pageContext = !anchor && primary && samePage && !privateForm && !root.closest("dialog,[role='dialog']");
1059
1059
  const images = /* @__PURE__ */ new Set(), urls = /* @__PURE__ */ new Set(), skus = /* @__PURE__ */ new Set(), variants = /* @__PURE__ */ new Set(), ids = /* @__PURE__ */ new Set();
@@ -1067,7 +1067,7 @@ function createProductIndex(options) {
1067
1067
  if (primary && own && !forms.includes(own)) return empty("unresolved");
1068
1068
  const checked = [...document.querySelectorAll('input[type="radio"]:checked')];
1069
1069
  if (checked.length > 32) return empty("ambiguous");
1070
- const controls = checked.map((node) => id(node.value)).filter((value) => value && bound.some((entry) => entry.variant === value));
1070
+ const controls = checked.map((node2) => id(node2.value)).filter((value) => value && bound.some((entry) => entry.variant === value));
1071
1071
  const selections = [.../* @__PURE__ */ new Set([...(own ? forms.filter((value) => value === own) : forms).map((form2) => form2.variant), ...controls])];
1072
1072
  const nativeConflict = selections.length && (selections.length !== 1 || currentSelector && !sameVariant(selections[0], currentSelector));
1073
1073
  const nativeVariant = samePage && selections.length === 1 && (!currentSelector || sameVariant(selections[0], currentSelector)) ? selections[0] : void 0;
@@ -1081,20 +1081,20 @@ function createProductIndex(options) {
1081
1081
  while ((next = walker.nextNode()) && nodes2.length <= 64) nodes2.push(next);
1082
1082
  if (nodes2.length > 64) break;
1083
1083
  if (!direct && card.matches("[data-product-id],[data-variant-id],[data-product_id],[data-variant_id]")) direct = card;
1084
- for (const node of nodes2) {
1085
- const image = publicURL(node.getAttribute("src") ?? void 0, location.href, true);
1084
+ for (const node2 of nodes2) {
1085
+ const image = publicURL(node2.getAttribute("src") ?? void 0, location.href, true);
1086
1086
  if (image) images.add(image);
1087
- for (const match of String(getComputedStyle(node).backgroundImage ?? "").matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
1087
+ for (const match of String(getComputedStyle(node2).backgroundImage ?? "").matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
1088
1088
  const value = publicURL(match[1], location.href, true);
1089
1089
  if (value) images.add(value);
1090
1090
  }
1091
- const url = publicURL(node.getAttribute("href") ?? void 0);
1091
+ const url = publicURL(node2.getAttribute("href") ?? void 0);
1092
1092
  if (url) urls.add(url);
1093
- const sku = id(node.getAttribute("data-sku") ?? (node.getAttribute("itemprop") === "sku" ? node.getAttribute("content") : null));
1093
+ const sku = id(node2.getAttribute("data-sku") ?? (node2.getAttribute("itemprop") === "sku" ? node2.getAttribute("content") : null));
1094
1094
  if (sku) skus.add(sku);
1095
- const variant = id(node.getAttribute("data-variant-id") ?? node.getAttribute("data-variant_id"));
1095
+ const variant = id(node2.getAttribute("data-variant-id") ?? node2.getAttribute("data-variant_id"));
1096
1096
  if (variant) variants.add(variant);
1097
- const product = id(node.getAttribute("data-product-id") ?? node.getAttribute("data-product_id") ?? (node.getAttribute("itemprop") === "productID" ? node.getAttribute("content") : null));
1097
+ const product = id(node2.getAttribute("data-product-id") ?? node2.getAttribute("data-product_id") ?? (node2.getAttribute("itemprop") === "productID" ? node2.getAttribute("content") : null));
1098
1098
  if (product) ids.add(product);
1099
1099
  }
1100
1100
  if (targetURL || images.size && urls.size) break;
@@ -1133,8 +1133,8 @@ function createProductIndex(options) {
1133
1133
  const exactURL = !!desired && (entry.url === desired || !!selected && entry.url?.split("?")[0] === desired.split("?")[0] && [entry.variant, entry.sku, ...variantValues].some((value) => sameVariant(value, selected)));
1134
1134
  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])`);
1135
1135
  const skuImage = token ? [...images].find((value) => !sharedImages.has(value) && token.test(new URL(value).pathname)) : void 0;
1136
- const title = normalized(entry.snapshot.name), titleOnPage = [...document.querySelectorAll("h1")].slice(0, 8).some((node) => node.getClientRects().length && normalized(node.textContent) === title);
1137
- 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);
1136
+ const title = normalized(entry.snapshot.name), titleOnPage = [...document.querySelectorAll("h1")].slice(0, 8).some((node2) => node2.getClientRects().length && normalized(node2.textContent) === title);
1137
+ 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);
1138
1138
  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));
1139
1139
  const corroborated = title && title === normalized(observedLabel) && observedPrice !== void 0 && entry.snapshot.price === observedPrice;
1140
1140
  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;
@@ -1238,13 +1238,13 @@ function productAt(target) {
1238
1238
  if (candidate.closest(privateRegion)) return null;
1239
1239
  const children = nodes(candidate, 64);
1240
1240
  if (!children) return null;
1241
- if (children.some((node) => node.matches("input,textarea,select,[contenteditable='true']"))) return null;
1242
- const images = children.filter((node) => node.matches(imageSelector));
1241
+ if (children.some((node2) => node2.matches("input,textarea,select,[contenteditable='true']"))) return null;
1242
+ const images = children.filter((node2) => node2.matches(imageSelector));
1243
1243
  if (images.length > 1) return null;
1244
1244
  if (images.length !== 1) continue;
1245
1245
  const name2 = label(images[0].getAttribute("alt") ?? images[0].getAttribute("aria-label"));
1246
- if (!name2 || !children.some((node) => node !== images[0] && !node.childElementCount && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden" && label(text(node)) === name2)) continue;
1247
- 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);
1246
+ if (!name2 || !children.some((node2) => node2 !== images[0] && !node2.childElementCount && node2.getClientRects().length && getComputedStyle(node2).visibility !== "hidden" && label(text(node2)) === name2)) continue;
1247
+ 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);
1248
1248
  if (!amounts.length || new Set(amounts.map((amount) => `${amount.price}:${amount.currency ?? ""}`)).size > 1) continue;
1249
1249
  const siblings = candidate.parentElement?.children;
1250
1250
  if (siblings && siblings.length > 64) return null;
@@ -1252,14 +1252,14 @@ function productAt(target) {
1252
1252
  if (sibling === candidate) continue;
1253
1253
  const entries = nodes(sibling, 64);
1254
1254
  if (!entries) return null;
1255
- if (entries.some((node) => node.matches(imageSelector) && label(node.getAttribute("alt") ?? node.getAttribute("aria-label")) === name2)) return null;
1255
+ if (entries.some((node2) => node2.matches(imageSelector) && label(node2.getAttribute("alt") ?? node2.getAttribute("aria-label")) === name2)) return null;
1256
1256
  }
1257
1257
  return { node: candidate, label: name2, ...amounts[0] };
1258
1258
  }
1259
1259
  return null;
1260
1260
  }
1261
1261
  function quantity(product) {
1262
- 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);
1262
+ 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);
1263
1263
  const unique = [...new Set(values)];
1264
1264
  return unique.length === 1 ? Number(unique[0]) : null;
1265
1265
  }
@@ -1269,43 +1269,43 @@ function createDOMCart(options) {
1269
1269
  function cart(wanted) {
1270
1270
  const markers = [], possible = document.querySelectorAll("h1,h2,h3,h4,h5,h6,span,[aria-label],aside,[role='dialog']");
1271
1271
  for (let index = 0; index < Math.min(possible.length, 128); index++) {
1272
- const node2 = possible[index];
1273
- if (!node2.childElementCount && /^(?:shopping )?(?:cart|bag)$/i.test(text(node2) ?? "")) markers.push(node2);
1272
+ const node3 = possible[index];
1273
+ if (!node3.childElementCount && /^(?:shopping )?(?:cart|bag)$/i.test(text(node3) ?? "")) markers.push(node3);
1274
1274
  }
1275
1275
  const candidates = /* @__PURE__ */ new Set();
1276
1276
  for (const marker of markers.slice(0, 8)) {
1277
- let node2 = marker.parentElement;
1278
- for (let depth = 0; node2 && node2 !== document.body && depth < 6; depth++, node2 = node2.parentElement) {
1279
- const children = nodes(node2, 256);
1277
+ let node3 = marker.parentElement;
1278
+ for (let depth = 0; node3 && node3 !== document.body && depth < 6; depth++, node3 = node3.parentElement) {
1279
+ const children = nodes(node3, 256);
1280
1280
  if (!children) break;
1281
1281
  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;
1282
1282
  const buttons = children.filter((child) => child.matches("button,[role='button']"));
1283
1283
  if (!buttons.some((button) => /^(?:proceed to )?checkout$/i.test(text(button) ?? ""))) continue;
1284
- if (buttons.some((button) => /\badd\b.*\bcart\b/i.test(text(button) ?? "")) || node2.closest("[hidden],[aria-hidden='true'],[aria-busy='true']")) break;
1285
- const style = getComputedStyle(node2);
1284
+ if (buttons.some((button) => /\badd\b.*\bcart\b/i.test(text(button) ?? "")) || node3.closest("[hidden],[aria-hidden='true'],[aria-busy='true']")) break;
1285
+ const style = getComputedStyle(node3);
1286
1286
  if (style.display === "none" || style.visibility === "hidden") break;
1287
- candidates.add(node2);
1287
+ candidates.add(node3);
1288
1288
  break;
1289
1289
  }
1290
1290
  }
1291
1291
  if (candidates.size !== 1) return null;
1292
- const node = [...candidates][0], images = (nodes(node, 256) ?? []).filter((child) => child.matches(imageSelector));
1292
+ const node2 = [...candidates][0], images = (nodes(node2, 256) ?? []).filter((child) => child.matches(imageSelector));
1293
1293
  if (images.length > 16) return null;
1294
- 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;
1294
+ 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;
1295
1295
  const seen = /* @__PURE__ */ new Set();
1296
1296
  let count = 0;
1297
1297
  for (const image of images) {
1298
1298
  const product = productAt(image), amount = product ? quantity(product) : null;
1299
- if (!product || !node.contains(product.node) || amount === null || seen.has(product.label)) return null;
1299
+ if (!product || !node2.contains(product.node) || amount === null || seen.has(product.label)) return null;
1300
1300
  seen.add(product.label);
1301
1301
  if (product.label === wanted) count = amount;
1302
1302
  }
1303
- let ref = regions.get(node);
1303
+ let ref = regions.get(node2);
1304
1304
  if (!ref) {
1305
1305
  ref = crypto.randomUUID();
1306
- regions.set(node, ref);
1306
+ regions.set(node2, ref);
1307
1307
  }
1308
- return { node, ref, quantity: count };
1308
+ return { node: node2, ref, quantity: count };
1309
1309
  }
1310
1310
  function remove(key) {
1311
1311
  const item = pending.get(key);
@@ -1440,7 +1440,7 @@ function observePage(options) {
1440
1440
  }
1441
1441
  function click(event) {
1442
1442
  const epoch = options.epoch();
1443
- if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((node) => node instanceof Element && node.hasAttribute("data-sagepilot-popup"))) return;
1443
+ if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((node2) => node2 instanceof Element && node2.hasAttribute("data-sagepilot-popup"))) return;
1444
1444
  if (pendingClicks >= 128) {
1445
1445
  options.dropped();
1446
1446
  return;
@@ -1448,12 +1448,12 @@ function observePage(options) {
1448
1448
  try {
1449
1449
  const target = event.target.closest("button,a,input,select,textarea,[role='button']") ?? event.target;
1450
1450
  const path = [];
1451
- let node = target;
1452
- for (let depth = 0; node && depth < 6; depth++, node = node.parentElement) {
1453
- const role = node.getAttribute("role"), type = node.getAttribute("type"), parent = node.parentElement;
1454
- const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children, node) : -1;
1451
+ let node2 = target;
1452
+ for (let depth = 0; node2 && depth < 6; depth++, node2 = node2.parentElement) {
1453
+ const role = node2.getAttribute("role"), type = node2.getAttribute("type"), parent = node2.parentElement;
1454
+ const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children, node2) : -1;
1455
1455
  path.push({
1456
- tag: node.tagName.toLowerCase(),
1456
+ tag: node2.tagName.toLowerCase(),
1457
1457
  sibling_index: sibling,
1458
1458
  ...role && roles.has(role) ? { role } : {},
1459
1459
  ...type && /^(button|submit|reset|checkbox|radio)$/.test(type) ? { type } : {}
@@ -2027,6 +2027,153 @@ function createJournal(scope) {
2027
2027
  };
2028
2028
  }
2029
2029
 
2030
+ // src/popup-design.ts
2031
+ var layouts = ["centered", "image_left", "image_right", "bottom_sheet"];
2032
+ var builtIns = /* @__PURE__ */ new Set(["logo", "title", "body", "hero", "form"]);
2033
+ var defaultBlocks = [{ kind: "logo" }, { kind: "hero" }, { kind: "title" }, { kind: "body" }, { kind: "form" }];
2034
+ function asset(value) {
2035
+ if (typeof value !== "string" || value.length > 1e3 || !value.startsWith("https://") || /[\s\\'"<>]/.test(value)) return false;
2036
+ try {
2037
+ const url = new URL(value);
2038
+ return url.protocol === "https:" && !!url.hostname && url.port !== "0" && !url.username && !url.password && !url.hash;
2039
+ } catch {
2040
+ return false;
2041
+ }
2042
+ }
2043
+ function validPopupDesign(design) {
2044
+ if (design == null) return true;
2045
+ const color = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
2046
+ const integer = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
2047
+ 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;
2048
+ if (design.blocks === null) return true;
2049
+ if (!Array.isArray(design.blocks) || design.blocks.length > 12) return false;
2050
+ const seen = /* @__PURE__ */ new Set();
2051
+ return design.blocks.every((block) => {
2052
+ if (!block || typeof block !== "object") return false;
2053
+ if (builtIns.has(block.kind)) {
2054
+ if (seen.has(block.kind)) return false;
2055
+ seen.add(block.kind);
2056
+ return true;
2057
+ }
2058
+ if (block.kind === "heading" || block.kind === "text") return typeof block.text === "string" && block.text.length > 0 && block.text.length <= 600;
2059
+ if (block.kind === "image") return asset(block.url) && typeof block.alt === "string" && block.alt.length <= 200;
2060
+ if (block.kind === "link") return asset(block.url) && typeof block.text === "string" && block.text.length > 0 && block.text.length <= 120;
2061
+ return false;
2062
+ }) && seen.has("form");
2063
+ }
2064
+ function node(tag, value) {
2065
+ const result = document.createElement(tag);
2066
+ if (value !== void 0) result.textContent = value;
2067
+ return result;
2068
+ }
2069
+ function renderPopupDesign(panel, host, config, form) {
2070
+ const design = config.design;
2071
+ const blocks = design?.blocks ?? defaultBlocks;
2072
+ const content = node("div");
2073
+ content.className = "content";
2074
+ const hero = design?.image_url && blocks.some((block) => block.kind === "hero") ? popupImage(design.image_url, design.image_alt, "hero") : null;
2075
+ const split = hero && (design?.layout === "image_left" || design?.layout === "image_right");
2076
+ if (split) {
2077
+ const side = node("div");
2078
+ side.className = "side-art";
2079
+ side.append(hero);
2080
+ panel.append(side);
2081
+ }
2082
+ let heading = false;
2083
+ for (const block of blocks) {
2084
+ switch (block.kind) {
2085
+ case "logo":
2086
+ if (config.logo_url) content.append(popupImage(config.logo_url, "", "logo"));
2087
+ break;
2088
+ case "hero":
2089
+ if (hero && !split) content.append(hero);
2090
+ break;
2091
+ case "title":
2092
+ case "heading": {
2093
+ const title = node(heading ? "h3" : "h2", block.kind === "heading" ? block.text : config.title);
2094
+ if (!heading) {
2095
+ title.id = "sage-popup-title";
2096
+ heading = true;
2097
+ }
2098
+ content.append(title);
2099
+ break;
2100
+ }
2101
+ case "body":
2102
+ content.append(node("p", config.body));
2103
+ break;
2104
+ case "text":
2105
+ content.append(node("p", block.text));
2106
+ break;
2107
+ case "image":
2108
+ content.append(popupImage(block.url, block.alt, "block-image"));
2109
+ break;
2110
+ case "link": {
2111
+ const link = node("a", block.text);
2112
+ link.href = block.url;
2113
+ link.target = "_blank";
2114
+ link.rel = "noopener noreferrer";
2115
+ content.append(link);
2116
+ break;
2117
+ }
2118
+ case "form":
2119
+ content.append(form);
2120
+ break;
2121
+ }
2122
+ }
2123
+ panel.append(content);
2124
+ if (heading) panel.setAttribute("aria-labelledby", "sage-popup-title");
2125
+ else panel.setAttribute("aria-label", config.title);
2126
+ if (!design) return () => {
2127
+ };
2128
+ panel.dataset.design = "";
2129
+ panel.dataset.layout = split ? design.layout : design.layout === "bottom_sheet" ? "bottom_sheet" : "centered";
2130
+ if (panel.dataset.layout === "bottom_sheet") {
2131
+ host.style.alignItems = "end";
2132
+ host.style.paddingBottom = "0";
2133
+ }
2134
+ for (const [key, value] of Object.entries({
2135
+ "--popup-surface": design.background_color,
2136
+ "--popup-text": design.text_color,
2137
+ "--popup-button-text": design.button_text_color,
2138
+ "--popup-radius": design.border_radius + "px",
2139
+ "--popup-padding": design.padding + "px",
2140
+ "--popup-width": design.width + "px"
2141
+ })) panel.style.setProperty(key, value);
2142
+ let face;
2143
+ const fallback = design.font_family ? JSON.stringify(design.font_family) + ", system-ui, sans-serif" : "system-ui, sans-serif";
2144
+ if (design.font_url && typeof FontFace === "function" && document.fonts) {
2145
+ try {
2146
+ face = new FontFace("sage-popup-" + crypto.randomUUID(), "url(" + JSON.stringify(design.font_url) + ")");
2147
+ document.fonts.add(face);
2148
+ panel.style.fontFamily = JSON.stringify(face.family) + ", " + fallback;
2149
+ } catch {
2150
+ panel.style.fontFamily = fallback;
2151
+ }
2152
+ } else panel.style.fontFamily = fallback;
2153
+ return () => {
2154
+ if (face) document.fonts.delete(face);
2155
+ };
2156
+ }
2157
+ function popupImage(url, alt, className) {
2158
+ const image = node("img");
2159
+ image.referrerPolicy = "no-referrer";
2160
+ image.alt = alt;
2161
+ image.className = className;
2162
+ image.src = url;
2163
+ return image;
2164
+ }
2165
+ var popupDesignStyles = [
2166
+ ".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}",
2167
+ ".panel[data-design] .content{min-width:0;padding:var(--popup-padding)}",
2168
+ ".panel[data-design] :is(p,a){color:inherit;white-space:pre-wrap}",
2169
+ ".panel[data-design] input:not([type=checkbox]){color:inherit;background:transparent;border-color:currentColor}",
2170
+ ".panel[data-design] button[type=submit]{color:var(--popup-button-text)}",
2171
+ ".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)}",
2172
+ ".panel[data-layout=bottom_sheet]{border-end-start-radius:0;border-end-end-radius:0}",
2173
+ ".panel .side-art{position:relative;min-width:0;overflow:hidden}.panel .side-art>.hero{border-radius:0}.panel[data-design] .close{z-index:1}",
2174
+ "@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}}"
2175
+ ].join("");
2176
+
2030
2177
  // src/popup.ts
2031
2178
  function popupCompletionKey(workspaceId, installationId) {
2032
2179
  return `sagepilot:sdk-popup:${workspaceId}:${installationId}:completed`;
@@ -2041,7 +2188,7 @@ function valid(value) {
2041
2188
  return false;
2042
2189
  }
2043
2190
  }
2044
- return true;
2191
+ return validPopupDesign(value.design);
2045
2192
  }
2046
2193
  function createPopup(options) {
2047
2194
  let config = null, host = null;
@@ -2074,9 +2221,9 @@ function createPopup(options) {
2074
2221
  return !raw || !Number.isFinite(last) || Date.now() - last >= config.frequency_hours * 36e5;
2075
2222
  }
2076
2223
  function element(tag, text2) {
2077
- const node = document.createElement(tag);
2078
- if (text2 !== void 0) node.textContent = text2;
2079
- return node;
2224
+ const node2 = document.createElement(tag);
2225
+ if (text2 !== void 0) node2.textContent = text2;
2226
+ return node2;
2080
2227
  }
2081
2228
  function show() {
2082
2229
  if (!eligible() || !config) return;
@@ -2088,12 +2235,12 @@ function createPopup(options) {
2088
2235
  const shadow = host.attachShadow({ mode: "open" });
2089
2236
  const style = element("style");
2090
2237
  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}}`;
2238
+ style.textContent += popupDesignStyles;
2091
2239
  shadow.append(style);
2092
2240
  const panel = element("section");
2093
2241
  panel.className = "panel";
2094
2242
  panel.setAttribute("role", "dialog");
2095
2243
  panel.setAttribute("aria-modal", "true");
2096
- panel.setAttribute("aria-labelledby", "sage-popup-title");
2097
2244
  const close = element("button", "\xD7");
2098
2245
  close.type = "button";
2099
2246
  close.className = "close";
@@ -2104,17 +2251,6 @@ function createPopup(options) {
2104
2251
  }
2105
2252
  close.addEventListener("click", dismiss);
2106
2253
  panel.append(close);
2107
- if (published.logo_url) {
2108
- const logo = element("img");
2109
- logo.src = published.logo_url;
2110
- logo.alt = "";
2111
- logo.className = "logo";
2112
- logo.referrerPolicy = "no-referrer";
2113
- panel.append(logo);
2114
- }
2115
- const title = element("h2", published.title);
2116
- title.id = "sage-popup-title";
2117
- panel.append(title, element("p", published.body));
2118
2254
  const form = element("form"), status = element("p");
2119
2255
  status.className = "status";
2120
2256
  status.setAttribute("role", "status");
@@ -2138,7 +2274,7 @@ function createPopup(options) {
2138
2274
  const submit = element("button", published.button_text);
2139
2275
  submit.type = "submit";
2140
2276
  form.append(submit, status);
2141
- panel.append(form);
2277
+ const cleanupDesign = renderPopupDesign(panel, host, published, form);
2142
2278
  shadow.append(panel);
2143
2279
  document.body.append(host);
2144
2280
  options.storage.setItem(shownKey, String(Date.now()));
@@ -2155,8 +2291,8 @@ function createPopup(options) {
2155
2291
  if (challenge) {
2156
2292
  await options.identify(challenge, inputs.get("code").value);
2157
2293
  if (revision !== generation) return;
2158
- form.replaceChildren(element("p", published.success_text), close);
2159
- close.className = "close";
2294
+ form.replaceChildren(element("p", published.success_text));
2295
+ close.focus();
2160
2296
  } else {
2161
2297
  const contact = { submission_id: submissionId, popup_revision: published.revision, marketing_consent: true };
2162
2298
  for (const field of ["name", "email", "phone"]) {
@@ -2198,7 +2334,10 @@ function createPopup(options) {
2198
2334
  form.replaceChildren(label2, submit, restart, status);
2199
2335
  status.textContent = "Check your email for the six-digit code.";
2200
2336
  code.focus();
2201
- } else form.replaceChildren(element("p", published.success_text));
2337
+ } else {
2338
+ form.replaceChildren(element("p", published.success_text));
2339
+ close.focus();
2340
+ }
2202
2341
  }
2203
2342
  } catch (error) {
2204
2343
  if (revision === generation) status.textContent = challenge ? "Verification failed. Check the code and try again." : "We couldn't save the form. Please try again.";
@@ -2216,7 +2355,7 @@ function createPopup(options) {
2216
2355
  return;
2217
2356
  }
2218
2357
  if (event.key !== "Tab") return;
2219
- const focusable = [...shadow.querySelectorAll("button:not(:disabled),input:not(:disabled)")];
2358
+ const focusable = [...shadow.querySelectorAll("button:not(:disabled),input:not(:disabled),a[href]")];
2220
2359
  const first = focusable[0], last = focusable[focusable.length - 1];
2221
2360
  if (event.shiftKey && shadow.activeElement === first) {
2222
2361
  event.preventDefault();
@@ -2228,6 +2367,7 @@ function createPopup(options) {
2228
2367
  }
2229
2368
  shadow.addEventListener("keydown", keydown);
2230
2369
  cleanup = () => {
2370
+ cleanupDesign();
2231
2371
  form.removeEventListener("submit", submitForm);
2232
2372
  shadow.removeEventListener("keydown", keydown);
2233
2373
  };