@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/README.md
CHANGED
|
@@ -27,6 +27,19 @@ ESM, CommonJS and type builds are produced alongside the standalone `dist/sagepi
|
|
|
27
27
|
|
|
28
28
|
Both local permission and the server's `collection_enabled` must allow capture. Server settings refresh at least once per minute while started, including when collection is off so popup publication still propagates. `refreshConfig()` applies changes immediately. Refresh failures suspend capture until a successful refresh. Ingress tokens are short lived; Event Handler performs no database lookup and FJE checks current installation policy before processing. A queue admission receipt is not event normalization or journey execution.
|
|
29
29
|
|
|
30
|
+
## Website popup design
|
|
31
|
+
|
|
32
|
+
In Settings → SDK Events, select a web installation and edit its Website popup. Apply the
|
|
33
|
+
workspace's saved Brand Identity or adjust layout, artwork, typography, colors,
|
|
34
|
+
dimensions and content order directly. Upload artwork or use an HTTPS image URL.
|
|
35
|
+
Preview desktop and mobile, save the
|
|
36
|
+
draft, then publish. The same installation fetches the published revision.
|
|
37
|
+
|
|
38
|
+
The Portal can paste or read a local `.html` file and convert supported
|
|
39
|
+
headings, paragraphs, images and links into editable blocks. Include one
|
|
40
|
+
`<sagepilot-form></sagepilot-form>` placeholder. Styles use the visual
|
|
41
|
+
controls and the SDK owns contact submission and optional email verification.
|
|
42
|
+
|
|
30
43
|
## Captured evidence
|
|
31
44
|
|
|
32
45
|
| Source | Supported evidence |
|
package/dist/index.cjs
CHANGED
|
@@ -364,7 +364,7 @@ function createSagepilotInstallationClient(options) {
|
|
|
364
364
|
|
|
365
365
|
// package.json
|
|
366
366
|
var name = "@sagepilot-ai/web-events-addon";
|
|
367
|
-
var version = "0.2.
|
|
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 @@ function labeledProductID() {
|
|
|
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((
|
|
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 @@ function createProductIndex(options) {
|
|
|
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((
|
|
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 @@ function createProductIndex(options) {
|
|
|
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((
|
|
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 @@ function createProductIndex(options) {
|
|
|
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((
|
|
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 @@ function createProductIndex(options) {
|
|
|
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
|
|
1111
|
-
const image = publicURL(
|
|
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(
|
|
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(
|
|
1117
|
+
const url = publicURL(node2.getAttribute("href") ?? void 0);
|
|
1118
1118
|
if (url) urls.add(url);
|
|
1119
|
-
const sku = id(
|
|
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(
|
|
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(
|
|
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 @@ function createProductIndex(options) {
|
|
|
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((
|
|
1163
|
-
const groupOnPage = entry.group?.url === canonical && samePage && entry.snapshot.metadata.provenance.source_ref === current && [...document.querySelectorAll("h1")].slice(0, 8).some((
|
|
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 @@ function productAt(target) {
|
|
|
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((
|
|
1268
|
-
const images = children.filter((
|
|
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((
|
|
1273
|
-
const amounts = children.filter((
|
|
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 @@ function productAt(target) {
|
|
|
1278
1278
|
if (sibling === candidate) continue;
|
|
1279
1279
|
const entries = nodes(sibling, 64);
|
|
1280
1280
|
if (!entries) return null;
|
|
1281
|
-
if (entries.some((
|
|
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((
|
|
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 @@ function createDOMCart(options) {
|
|
|
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
|
|
1299
|
-
if (!
|
|
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
|
|
1304
|
-
for (let depth = 0;
|
|
1305
|
-
const children = nodes(
|
|
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) ?? "")) ||
|
|
1311
|
-
const style = getComputedStyle(
|
|
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(
|
|
1313
|
+
candidates.add(node3);
|
|
1314
1314
|
break;
|
|
1315
1315
|
}
|
|
1316
1316
|
}
|
|
1317
1317
|
if (candidates.size !== 1) return null;
|
|
1318
|
-
const
|
|
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(
|
|
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 || !
|
|
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(
|
|
1329
|
+
let ref = regions.get(node2);
|
|
1330
1330
|
if (!ref) {
|
|
1331
1331
|
ref = crypto.randomUUID();
|
|
1332
|
-
regions.set(
|
|
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 @@ function observePage(options) {
|
|
|
1466
1466
|
}
|
|
1467
1467
|
function click(event) {
|
|
1468
1468
|
const epoch = options.epoch();
|
|
1469
|
-
if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((
|
|
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 @@ function observePage(options) {
|
|
|
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
|
|
1478
|
-
for (let depth = 0;
|
|
1479
|
-
const role =
|
|
1480
|
-
const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children,
|
|
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:
|
|
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 @@ function createJournal(scope) {
|
|
|
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 @@ function valid(value) {
|
|
|
2067
2214
|
return false;
|
|
2068
2215
|
}
|
|
2069
2216
|
}
|
|
2070
|
-
return
|
|
2217
|
+
return validPopupDesign(value.design);
|
|
2071
2218
|
}
|
|
2072
2219
|
function createPopup(options) {
|
|
2073
2220
|
let config = null, host = null;
|
|
@@ -2100,9 +2247,9 @@ function createPopup(options) {
|
|
|
2100
2247
|
return !raw || !Number.isFinite(last) || Date.now() - last >= config.frequency_hours * 36e5;
|
|
2101
2248
|
}
|
|
2102
2249
|
function element(tag, text2) {
|
|
2103
|
-
const
|
|
2104
|
-
if (text2 !== void 0)
|
|
2105
|
-
return
|
|
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 @@ function createPopup(options) {
|
|
|
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 @@ function createPopup(options) {
|
|
|
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 @@ function createPopup(options) {
|
|
|
2164
2300
|
const submit = element("button", published.button_text);
|
|
2165
2301
|
submit.type = "submit";
|
|
2166
2302
|
form.append(submit, status);
|
|
2167
|
-
panel
|
|
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 @@ function createPopup(options) {
|
|
|
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)
|
|
2185
|
-
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 @@ function createPopup(options) {
|
|
|
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
|
|
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 @@ function createPopup(options) {
|
|
|
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 @@ function createPopup(options) {
|
|
|
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
|
};
|