@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/README.md +13 -0
- package/dist/index.cjs +201 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +201 -61
- package/dist/index.js.map +1 -1
- package/dist/sagepilot-web-events.global.js +201 -61
- package/dist/sagepilot-web-events.global.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
1085
|
-
const image = publicURL(
|
|
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(
|
|
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(
|
|
1091
|
+
const url = publicURL(node2.getAttribute("href") ?? void 0);
|
|
1092
1092
|
if (url) urls.add(url);
|
|
1093
|
-
const sku = id(
|
|
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(
|
|
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(
|
|
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((
|
|
1137
|
-
const groupOnPage = entry.group?.url === canonical && samePage && entry.snapshot.metadata.provenance.source_ref === current && [...document.querySelectorAll("h1")].slice(0, 8).some((
|
|
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((
|
|
1242
|
-
const images = children.filter((
|
|
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((
|
|
1247
|
-
const amounts = children.filter((
|
|
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((
|
|
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((
|
|
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
|
|
1273
|
-
if (!
|
|
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
|
|
1278
|
-
for (let depth = 0;
|
|
1279
|
-
const children = nodes(
|
|
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) ?? "")) ||
|
|
1285
|
-
const style = getComputedStyle(
|
|
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(
|
|
1287
|
+
candidates.add(node3);
|
|
1288
1288
|
break;
|
|
1289
1289
|
}
|
|
1290
1290
|
}
|
|
1291
1291
|
if (candidates.size !== 1) return null;
|
|
1292
|
-
const
|
|
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(
|
|
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 || !
|
|
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(
|
|
1303
|
+
let ref = regions.get(node2);
|
|
1304
1304
|
if (!ref) {
|
|
1305
1305
|
ref = crypto.randomUUID();
|
|
1306
|
-
regions.set(
|
|
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((
|
|
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
|
|
1452
|
-
for (let depth = 0;
|
|
1453
|
-
const role =
|
|
1454
|
-
const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children,
|
|
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:
|
|
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
|
|
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
|
|
2078
|
-
if (text2 !== void 0)
|
|
2079
|
-
return
|
|
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
|
|
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)
|
|
2159
|
-
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
|
|
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
|
};
|