@lime-bundles/widget 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.cjs +403 -70
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +406 -70
- package/dist/lime-bundle.global.js +169 -33
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ document.querySelector("lime-bundle").addEventListener(
|
|
|
58
58
|
| `storefront-token` | ✓ | Storefront Access Token from `/app/settings/headless`. |
|
|
59
59
|
| `bundle-gid` | | Render one specific bundle. Overrides auto-detect. |
|
|
60
60
|
| `product-handle` | | Render bundles for this handle. Overrides URL detection. |
|
|
61
|
+
| `product-id` | | The product this embed is standing on — full GID or bare numeric id. Volume bundles price and sell this product; falls back to the handle cascade, then the bundle's first product. |
|
|
61
62
|
| `app-url` | | Enables impression + add-to-cart analytics when set. |
|
|
62
63
|
| `analytics` | | `"false"` suppresses analytics even when `app-url` is set. |
|
|
63
64
|
| `locale` | | BCP-47 tag forwarded to the Storefront API. |
|
|
@@ -67,6 +68,8 @@ document.querySelector("lime-bundle").addEventListener(
|
|
|
67
68
|
|
|
68
69
|
**Product resolution** (when `bundle-gid` is absent): explicit `product-handle` → `<meta name="shopify:product-handle">` → `/products/<handle>` URL segment.
|
|
69
70
|
|
|
71
|
+
**Multi-product binding**: a volume bundle can span several products, and the widget prices and sells the one the shopper is looking at: explicit `product-id` → the handle cascade above → the bundle's first product. On a pinned `bundle-gid` embed outside a `/products/<handle>` URL, set `product-id` (or `product-handle`) or the first product is what gets sold.
|
|
72
|
+
|
|
70
73
|
Changing any attribute at runtime re-fetches and re-renders.
|
|
71
74
|
|
|
72
75
|
### B2B buyer identity (programmatic property only)
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,28 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
// src/lime-bundle.ts
|
|
29
29
|
var import_core6 = require("@lime-bundles/core");
|
|
30
30
|
|
|
31
|
+
// ../render/src/host.ts
|
|
32
|
+
function intlFormatMoney(currencyCode) {
|
|
33
|
+
return (cents) => {
|
|
34
|
+
try {
|
|
35
|
+
return new Intl.NumberFormat(void 0, {
|
|
36
|
+
style: "currency",
|
|
37
|
+
currency: currencyCode
|
|
38
|
+
}).format(cents / 100);
|
|
39
|
+
} catch {
|
|
40
|
+
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
45
|
+
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
46
|
+
function bundleLineAttributes(bundleGid, bundleType) {
|
|
47
|
+
return [
|
|
48
|
+
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
49
|
+
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
// ../render/src/adapt.ts
|
|
32
54
|
var import_core = require("@lime-bundles/core");
|
|
33
55
|
function numericId(gid) {
|
|
@@ -40,6 +62,14 @@ function toCents(amount) {
|
|
|
40
62
|
const n = parseFloat(amount);
|
|
41
63
|
return Number.isFinite(n) ? Math.round(n * 100) : 0;
|
|
42
64
|
}
|
|
65
|
+
function imageAspectRatio(image) {
|
|
66
|
+
const w = image?.width;
|
|
67
|
+
const h = image?.height;
|
|
68
|
+
if (typeof w !== "number" || typeof h !== "number" || w <= 0 || h <= 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return w / h;
|
|
72
|
+
}
|
|
43
73
|
function adaptVariant(v, requiredQty, fallbackCurrency) {
|
|
44
74
|
return {
|
|
45
75
|
id: numericId(v.id),
|
|
@@ -81,6 +111,7 @@ function adaptProduct(product, bundleId, opts) {
|
|
|
81
111
|
title: product.title,
|
|
82
112
|
url: product.handle ? "/products/" + product.handle : null,
|
|
83
113
|
featuredImage: product.featuredImage?.url ?? null,
|
|
114
|
+
featuredImageRatio: imageAspectRatio(product.featuredImage),
|
|
84
115
|
quantity: productQty,
|
|
85
116
|
selectedVariantId: initial ? initial.id : "",
|
|
86
117
|
// Option names come off a variant, since the Storefront product query
|
|
@@ -230,28 +261,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
230
261
|
}
|
|
231
262
|
}
|
|
232
263
|
|
|
233
|
-
// ../render/src/host.ts
|
|
234
|
-
function intlFormatMoney(currencyCode) {
|
|
235
|
-
return (cents) => {
|
|
236
|
-
try {
|
|
237
|
-
return new Intl.NumberFormat(void 0, {
|
|
238
|
-
style: "currency",
|
|
239
|
-
currency: currencyCode
|
|
240
|
-
}).format(cents / 100);
|
|
241
|
-
} catch {
|
|
242
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
247
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
248
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
249
|
-
return [
|
|
250
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
251
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
252
|
-
];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
264
|
// ../render/src/product/option-selects.ts
|
|
256
265
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
257
266
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -447,7 +456,12 @@ function buildRowSkeleton(product, t, opts) {
|
|
|
447
456
|
}
|
|
448
457
|
row.setAttribute("data-product-id", String(product.productId));
|
|
449
458
|
if (opts.bogoRole) row.setAttribute("data-bogo-role", opts.bogoRole);
|
|
450
|
-
|
|
459
|
+
const thumb = box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
|
|
460
|
+
const ratio = product.featuredImageRatio;
|
|
461
|
+
if (opts.thumbnailRatio === "original" && ratio != null && ratio > 0) {
|
|
462
|
+
thumb.style.setProperty("--lb-row-thumb-ratio", String(ratio));
|
|
463
|
+
}
|
|
464
|
+
row.appendChild(thumb);
|
|
451
465
|
const info = box("div", "lb-bundle-product-info");
|
|
452
466
|
const title = document.createElement(opts.href ? "a" : "span");
|
|
453
467
|
title.className = "lb-bundle-product-name";
|
|
@@ -756,6 +770,16 @@ var DEFAULT_STRINGS = {
|
|
|
756
770
|
chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
|
|
757
771
|
stepOf: "Step __STEP__ of __TOTAL__",
|
|
758
772
|
stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
|
|
773
|
+
doorChoose: "Choose your products",
|
|
774
|
+
doorContinue: "Continue building",
|
|
775
|
+
doorQuickSteps: {
|
|
776
|
+
one: "__COUNT__ quick step",
|
|
777
|
+
other: "__COUNT__ quick steps"
|
|
778
|
+
},
|
|
779
|
+
doorStepsToGo: {
|
|
780
|
+
one: "__COUNT__ step to go",
|
|
781
|
+
other: "__COUNT__ steps to go"
|
|
782
|
+
},
|
|
759
783
|
each: " each",
|
|
760
784
|
buyQty: "Buy __COUNT__",
|
|
761
785
|
itemCount: {
|
|
@@ -1213,7 +1237,8 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1213
1237
|
const isOos = !product.variants.some((v) => v.available);
|
|
1214
1238
|
const row = buildRowSkeleton(product, t, {
|
|
1215
1239
|
isOos,
|
|
1216
|
-
href: product.url
|
|
1240
|
+
href: product.url,
|
|
1241
|
+
thumbnailRatio: wc.productList?.thumbnailRatio
|
|
1217
1242
|
});
|
|
1218
1243
|
shell.products.appendChild(row);
|
|
1219
1244
|
if (isOos) continue;
|
|
@@ -2939,9 +2964,9 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2939
2964
|
}
|
|
2940
2965
|
|
|
2941
2966
|
// src/renderers/volume.ts
|
|
2942
|
-
function renderVolumeBundle(container, bundle, onAddToCart) {
|
|
2967
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2943
2968
|
const wc = bundle.widgetConfig;
|
|
2944
|
-
const product = bundle.products
|
|
2969
|
+
const product = (0, import_core4.resolveContextProduct)(bundle.products, productContext);
|
|
2945
2970
|
if (!product) return;
|
|
2946
2971
|
const variants = product.variants.nodes;
|
|
2947
2972
|
const minTierQty = Math.min(
|
|
@@ -3212,7 +3237,8 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
3212
3237
|
href: side.url,
|
|
3213
3238
|
bogoRole: side.role,
|
|
3214
3239
|
inlineQuantity: isGet ? getQty : buyQty,
|
|
3215
|
-
badgeText: isGet && !isOos ? badgeText : null
|
|
3240
|
+
badgeText: isGet && !isOos ? badgeText : null,
|
|
3241
|
+
thumbnailRatio: wc.productList?.thumbnailRatio
|
|
3216
3242
|
});
|
|
3217
3243
|
shell.products.appendChild(row);
|
|
3218
3244
|
if (isOos) return;
|
|
@@ -3278,6 +3304,32 @@ function stepsCompletedLabel(completed, total, t) {
|
|
|
3278
3304
|
function moreToGoLabel(remaining, t) {
|
|
3279
3305
|
return remaining > 0 ? fill(t.moreToGo || "__COUNT__ more to go", { COUNT: remaining }) : t.complete || "Complete";
|
|
3280
3306
|
}
|
|
3307
|
+
function fillPlural(map, count, locale) {
|
|
3308
|
+
const form = new Intl.PluralRules(locale || "en").select(count);
|
|
3309
|
+
return fill(map[form] || map.other || "__COUNT__", { COUNT: count });
|
|
3310
|
+
}
|
|
3311
|
+
function doorLabel(anyPicks, t) {
|
|
3312
|
+
return anyPicks ? t.doorContinue || "Continue building" : t.doorChoose || "Choose your products";
|
|
3313
|
+
}
|
|
3314
|
+
function doorSubLabel(anyPicks, remaining, total, t) {
|
|
3315
|
+
if (!anyPicks) {
|
|
3316
|
+
const quick = t.doorQuickSteps;
|
|
3317
|
+
if (typeof quick === "string") return fill(quick, { COUNT: total });
|
|
3318
|
+
return fillPlural(
|
|
3319
|
+
quick || { one: "__COUNT__ quick step", other: "__COUNT__ quick steps" },
|
|
3320
|
+
total,
|
|
3321
|
+
t.locale
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
return fillPlural(
|
|
3325
|
+
t.doorStepsToGo || {
|
|
3326
|
+
one: "__COUNT__ step to go",
|
|
3327
|
+
other: "__COUNT__ steps to go"
|
|
3328
|
+
},
|
|
3329
|
+
remaining,
|
|
3330
|
+
t.locale
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3281
3333
|
|
|
3282
3334
|
// ../render/src/multi-step/rules.ts
|
|
3283
3335
|
function unitsInStep(selections, i) {
|
|
@@ -3339,20 +3391,28 @@ function updateStepGroups(deps) {
|
|
|
3339
3391
|
const { container, steps, selections, t } = deps;
|
|
3340
3392
|
for (let i = 0; i < steps.length; i++) {
|
|
3341
3393
|
const min = steps[i].minQuantity || 1;
|
|
3342
|
-
const
|
|
3343
|
-
'[data-step-
|
|
3394
|
+
const section = container.querySelector(
|
|
3395
|
+
'[data-step-group="' + i + '"]'
|
|
3344
3396
|
);
|
|
3345
|
-
if (
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
t
|
|
3350
|
-
);
|
|
3351
|
-
countEl.classList.toggle(
|
|
3352
|
-
"lb-multi-step__group-count--met",
|
|
3353
|
-
stepSatisfied(selections, steps, i)
|
|
3397
|
+
if (section) {
|
|
3398
|
+
section.setAttribute(
|
|
3399
|
+
"data-step-mode",
|
|
3400
|
+
selections[i].length > 0 ? "open" : "row"
|
|
3354
3401
|
);
|
|
3355
3402
|
}
|
|
3403
|
+
const countLabel = ofSelectedLabel(
|
|
3404
|
+
Math.min(unitsInStep(selections, i), min),
|
|
3405
|
+
min,
|
|
3406
|
+
t
|
|
3407
|
+
);
|
|
3408
|
+
const met = stepSatisfied(selections, steps, i);
|
|
3409
|
+
const countEls = container.querySelectorAll(
|
|
3410
|
+
'[data-step-count="' + i + '"]'
|
|
3411
|
+
);
|
|
3412
|
+
for (let c = 0; c < countEls.length; c++) {
|
|
3413
|
+
countEls[c].textContent = countLabel;
|
|
3414
|
+
countEls[c].classList.toggle("lb-multi-step__group-count--met", met);
|
|
3415
|
+
}
|
|
3356
3416
|
const slotsWrap = container.querySelector(
|
|
3357
3417
|
'[data-step-slots="' + i + '"]'
|
|
3358
3418
|
);
|
|
@@ -3374,14 +3434,50 @@ function updateStepGroups(deps) {
|
|
|
3374
3434
|
);
|
|
3375
3435
|
}
|
|
3376
3436
|
}
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3437
|
+
}
|
|
3438
|
+
function updateDoor(deps) {
|
|
3439
|
+
const { container, steps, selections, t } = deps;
|
|
3440
|
+
const door = container.querySelector("[data-step-door]");
|
|
3441
|
+
if (!door) return;
|
|
3442
|
+
let firstUnsatisfied = -1;
|
|
3443
|
+
let remaining = 0;
|
|
3444
|
+
let anyPicks = false;
|
|
3445
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3446
|
+
if (selections[i].length > 0) anyPicks = true;
|
|
3447
|
+
if (!stepSatisfied(selections, steps, i)) {
|
|
3448
|
+
remaining += 1;
|
|
3449
|
+
if (firstUnsatisfied === -1) firstUnsatisfied = i;
|
|
3383
3450
|
}
|
|
3384
3451
|
}
|
|
3452
|
+
if (firstUnsatisfied === -1) {
|
|
3453
|
+
door.style.display = "none";
|
|
3454
|
+
door.classList.add("lb-multi-step__door--hidden");
|
|
3455
|
+
const parent = door.parentElement;
|
|
3456
|
+
if (parent && parent.firstElementChild !== door) {
|
|
3457
|
+
parent.insertBefore(door, parent.firstElementChild);
|
|
3458
|
+
}
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
door.style.display = "";
|
|
3462
|
+
door.classList.remove("lb-multi-step__door--hidden");
|
|
3463
|
+
door.setAttribute("data-step-door", String(firstUnsatisfied));
|
|
3464
|
+
const label = doorLabel(anyPicks, t);
|
|
3465
|
+
const labelEl = door.querySelector(
|
|
3466
|
+
".lb-mix-match__add-product-label"
|
|
3467
|
+
);
|
|
3468
|
+
if (labelEl) labelEl.textContent = label;
|
|
3469
|
+
const subEl = door.querySelector(".lb-multi-step__door-sub");
|
|
3470
|
+
if (subEl) {
|
|
3471
|
+
subEl.textContent = doorSubLabel(anyPicks, remaining, steps.length, t);
|
|
3472
|
+
}
|
|
3473
|
+
const stepName = steps[firstUnsatisfied].name || "";
|
|
3474
|
+
door.setAttribute("aria-label", stepName ? label + ": " + stepName : label);
|
|
3475
|
+
const target = container.querySelector(
|
|
3476
|
+
'[data-step-group="' + firstUnsatisfied + '"]'
|
|
3477
|
+
);
|
|
3478
|
+
if (target && target.parentElement && target.previousElementSibling !== door) {
|
|
3479
|
+
target.parentElement.insertBefore(door, target);
|
|
3480
|
+
}
|
|
3385
3481
|
}
|
|
3386
3482
|
function updatePricing3(deps, data, calcDiscount, paint) {
|
|
3387
3483
|
const { container, steps, selections } = deps;
|
|
@@ -3825,10 +3921,12 @@ function createWizard(deps) {
|
|
|
3825
3921
|
setTimeout(() => {
|
|
3826
3922
|
if (!modalOpen) overlay.style.display = "none";
|
|
3827
3923
|
}, CLOSE_TRANSITION_MS2);
|
|
3828
|
-
const
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3924
|
+
const visible = (el2) => el2 && el2.offsetParent !== null ? el2 : null;
|
|
3925
|
+
const elToFocus = visible(
|
|
3926
|
+
container.querySelector(
|
|
3927
|
+
'[data-step-row="' + stepIndex + '"]'
|
|
3928
|
+
)
|
|
3929
|
+
) || visible(container.querySelector("[data-step-door]")) || deps.ctaButton;
|
|
3832
3930
|
if (elToFocus && typeof elToFocus.focus === "function") {
|
|
3833
3931
|
setTimeout(() => elToFocus.focus(), 0);
|
|
3834
3932
|
}
|
|
@@ -3881,6 +3979,29 @@ function createWizard(deps) {
|
|
|
3881
3979
|
}
|
|
3882
3980
|
|
|
3883
3981
|
// src/renderers/multi-step.ts
|
|
3982
|
+
function buildPlusIcon() {
|
|
3983
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
3984
|
+
const svg = document.createElementNS(NS, "svg");
|
|
3985
|
+
svg.setAttribute("width", "18");
|
|
3986
|
+
svg.setAttribute("height", "18");
|
|
3987
|
+
svg.setAttribute("viewBox", "0 0 18 18");
|
|
3988
|
+
svg.setAttribute("fill", "none");
|
|
3989
|
+
for (const [x1, y1, x2, y2] of [
|
|
3990
|
+
["9", "3", "9", "15"],
|
|
3991
|
+
["3", "9", "15", "9"]
|
|
3992
|
+
]) {
|
|
3993
|
+
const line = document.createElementNS(NS, "line");
|
|
3994
|
+
line.setAttribute("x1", x1);
|
|
3995
|
+
line.setAttribute("y1", y1);
|
|
3996
|
+
line.setAttribute("x2", x2);
|
|
3997
|
+
line.setAttribute("y2", y2);
|
|
3998
|
+
line.setAttribute("stroke", "currentColor");
|
|
3999
|
+
line.setAttribute("stroke-width", "2");
|
|
4000
|
+
line.setAttribute("stroke-linecap", "round");
|
|
4001
|
+
svg.appendChild(line);
|
|
4002
|
+
}
|
|
4003
|
+
return svg;
|
|
4004
|
+
}
|
|
3884
4005
|
function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3885
4006
|
const wc = bundle.widgetConfig;
|
|
3886
4007
|
const t = DEFAULT_STRINGS;
|
|
@@ -3935,31 +4056,65 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
3935
4056
|
shell.products.before(buildProgressBar(steps.length));
|
|
3936
4057
|
const groups = document.createElement("div");
|
|
3937
4058
|
groups.className = "lb-multi-step__groups";
|
|
4059
|
+
groups.setAttribute("data-step-groups", "");
|
|
4060
|
+
const door = document.createElement("button");
|
|
4061
|
+
door.type = "button";
|
|
4062
|
+
door.className = "lb-mix-match__add-product lb-multi-step__door";
|
|
4063
|
+
door.setAttribute("data-step-door", "0");
|
|
4064
|
+
const doorIcon = document.createElement("span");
|
|
4065
|
+
doorIcon.className = "lb-mix-match__add-product-icon";
|
|
4066
|
+
doorIcon.setAttribute("aria-hidden", "true");
|
|
4067
|
+
doorIcon.appendChild(buildPlusIcon());
|
|
4068
|
+
door.appendChild(doorIcon);
|
|
4069
|
+
const doorCopy = document.createElement("span");
|
|
4070
|
+
doorCopy.className = "lb-multi-step__door-copy";
|
|
4071
|
+
const doorLabel2 = document.createElement("span");
|
|
4072
|
+
doorLabel2.className = "lb-mix-match__add-product-label";
|
|
4073
|
+
doorCopy.appendChild(doorLabel2);
|
|
4074
|
+
const doorSub = document.createElement("span");
|
|
4075
|
+
doorSub.className = "lb-multi-step__door-sub";
|
|
4076
|
+
doorCopy.appendChild(doorSub);
|
|
4077
|
+
door.appendChild(doorCopy);
|
|
4078
|
+
groups.appendChild(door);
|
|
3938
4079
|
steps.forEach((step, i) => {
|
|
3939
|
-
const group = document.createElement("
|
|
4080
|
+
const group = document.createElement("section");
|
|
3940
4081
|
group.className = "lb-multi-step__group";
|
|
3941
4082
|
group.setAttribute("data-step-group", String(i));
|
|
4083
|
+
group.setAttribute("data-step-mode", "row");
|
|
4084
|
+
const buildCount = () => {
|
|
4085
|
+
const count = document.createElement("span");
|
|
4086
|
+
count.className = "lb-multi-step__group-count";
|
|
4087
|
+
count.setAttribute("data-step-count", String(i));
|
|
4088
|
+
return count;
|
|
4089
|
+
};
|
|
4090
|
+
const row = document.createElement("button");
|
|
4091
|
+
row.type = "button";
|
|
4092
|
+
row.className = "lb-multi-step__step-row";
|
|
4093
|
+
row.setAttribute("data-step-row", String(i));
|
|
4094
|
+
row.setAttribute("aria-label", "Choose products: " + step.name);
|
|
4095
|
+
const ix = document.createElement("span");
|
|
4096
|
+
ix.className = "lb-multi-step__step-ix";
|
|
4097
|
+
ix.setAttribute("aria-hidden", "true");
|
|
4098
|
+
ix.textContent = String(i + 1);
|
|
4099
|
+
row.appendChild(ix);
|
|
4100
|
+
const rowName = document.createElement("span");
|
|
4101
|
+
rowName.className = "lb-multi-step__group-name";
|
|
4102
|
+
rowName.textContent = step.name;
|
|
4103
|
+
row.appendChild(rowName);
|
|
4104
|
+
row.appendChild(buildCount());
|
|
4105
|
+
group.appendChild(row);
|
|
3942
4106
|
const heading = document.createElement("div");
|
|
3943
4107
|
heading.className = "lb-multi-step__group-heading";
|
|
3944
4108
|
const name = document.createElement("span");
|
|
3945
4109
|
name.className = "lb-multi-step__group-name";
|
|
3946
4110
|
name.textContent = step.name;
|
|
3947
4111
|
heading.appendChild(name);
|
|
3948
|
-
|
|
3949
|
-
count.className = "lb-multi-step__group-count";
|
|
3950
|
-
count.setAttribute("data-step-count", String(i));
|
|
3951
|
-
heading.appendChild(count);
|
|
4112
|
+
heading.appendChild(buildCount());
|
|
3952
4113
|
group.appendChild(heading);
|
|
3953
4114
|
const slots = document.createElement("div");
|
|
3954
|
-
slots.className = "lb-mix-match__slots lb-edge-fade";
|
|
4115
|
+
slots.className = "lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots";
|
|
3955
4116
|
slots.setAttribute("data-step-slots", String(i));
|
|
3956
4117
|
group.appendChild(slots);
|
|
3957
|
-
const trigger = document.createElement("button");
|
|
3958
|
-
trigger.type = "button";
|
|
3959
|
-
trigger.className = "lb-mix-match__add-product";
|
|
3960
|
-
trigger.setAttribute("data-step-trigger", String(i));
|
|
3961
|
-
trigger.textContent = "Add a product";
|
|
3962
|
-
group.appendChild(trigger);
|
|
3963
4118
|
groups.appendChild(group);
|
|
3964
4119
|
});
|
|
3965
4120
|
shell.products.replaceWith(groups);
|
|
@@ -3996,6 +4151,7 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
3996
4151
|
};
|
|
3997
4152
|
function updateAll() {
|
|
3998
4153
|
updateStepGroups(bodyDeps);
|
|
4154
|
+
updateDoor(bodyDeps);
|
|
3999
4155
|
updatePricing3(
|
|
4000
4156
|
bodyDeps,
|
|
4001
4157
|
{
|
|
@@ -4042,10 +4198,14 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
4042
4198
|
},
|
|
4043
4199
|
canRemove: (item) => canRemovePick2(selections, item, rulesMap)
|
|
4044
4200
|
});
|
|
4045
|
-
|
|
4201
|
+
door.addEventListener("click", () => {
|
|
4202
|
+
if (wizard.isOpen()) return;
|
|
4203
|
+
wizard.open(parseInt(door.getAttribute("data-step-door") ?? "", 10) || 0);
|
|
4204
|
+
});
|
|
4205
|
+
shell.root.querySelectorAll("[data-step-row]").forEach((el2) => {
|
|
4046
4206
|
el2.addEventListener("click", () => {
|
|
4047
4207
|
if (wizard.isOpen()) return;
|
|
4048
|
-
wizard.open(parseInt(el2.getAttribute("data-step-
|
|
4208
|
+
wizard.open(parseInt(el2.getAttribute("data-step-row") ?? "", 10) || 0);
|
|
4049
4209
|
});
|
|
4050
4210
|
});
|
|
4051
4211
|
shell.cta.addEventListener("click", () => {
|
|
@@ -6239,12 +6399,19 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6239
6399
|
* Canon: WIDGET-DESIGN.md (spacing scale, radius system, chip family).
|
|
6240
6400
|
*/
|
|
6241
6401
|
|
|
6242
|
-
/* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6402
|
+
/* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6403
|
+
One dashed door + one section per step. A step without picks shows a
|
|
6404
|
+
slim checklist row; a step with picks shows the heading + chosen
|
|
6405
|
+
cards. data-step-mode ("row" | "open") on the section picks the
|
|
6406
|
+
representation \u2014 both are in the markup, CSS hides the other, so the
|
|
6407
|
+
server-rendered untouched state IS the hydrated no-selection state.
|
|
6408
|
+
Spacing is per-pair rather than a flat gap: consecutive rows sit
|
|
6409
|
+
flush (hairline-separated), everything else breathes 16px. */
|
|
6243
6410
|
|
|
6244
6411
|
.lb-multi-step__groups {
|
|
6245
6412
|
display: flex;
|
|
6246
6413
|
flex-direction: column;
|
|
6247
|
-
gap:
|
|
6414
|
+
gap: 0;
|
|
6248
6415
|
}
|
|
6249
6416
|
|
|
6250
6417
|
.lb-multi-step__group {
|
|
@@ -6253,8 +6420,117 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6253
6420
|
gap: 8px;
|
|
6254
6421
|
}
|
|
6255
6422
|
|
|
6423
|
+
.lb-multi-step__group + .lb-multi-step__group {
|
|
6424
|
+
margin-top: 16px;
|
|
6425
|
+
}
|
|
6426
|
+
|
|
6427
|
+
/* A run of empty-step rows reads as one checklist: flush, hairlines
|
|
6428
|
+
between (the first row of a run keeps no border). */
|
|
6429
|
+
.lb-multi-step__group[data-step-mode="row"] + .lb-multi-step__group[data-step-mode="row"] {
|
|
6430
|
+
margin-top: 0;
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
.lb-multi-step__group[data-step-mode="row"]
|
|
6434
|
+
+ .lb-multi-step__group[data-step-mode="row"]
|
|
6435
|
+
.lb-multi-step__step-row {
|
|
6436
|
+
border-top: var(--lb-border-width) solid var(--lb-border);
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
/* Mode switch: a row-mode step hides its open chrome and vice versa. */
|
|
6440
|
+
.lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-heading,
|
|
6441
|
+
.lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-slots {
|
|
6442
|
+
display: none;
|
|
6443
|
+
}
|
|
6444
|
+
|
|
6445
|
+
.lb-multi-step__group[data-step-mode="open"] .lb-multi-step__step-row {
|
|
6446
|
+
display: none;
|
|
6447
|
+
}
|
|
6448
|
+
|
|
6449
|
+
/* Checklist row \u2014 the empty step's whole representation, and a tap
|
|
6450
|
+
target that deep-links the wizard to that step. Quiet by design: the
|
|
6451
|
+
door above it is the primary action. */
|
|
6452
|
+
.lb-multi-step__step-row {
|
|
6453
|
+
display: flex;
|
|
6454
|
+
align-items: center;
|
|
6455
|
+
gap: 12px;
|
|
6456
|
+
width: 100%;
|
|
6457
|
+
min-height: 48px;
|
|
6458
|
+
padding: 10px 2px;
|
|
6459
|
+
background: none;
|
|
6460
|
+
border: none;
|
|
6461
|
+
font-family: inherit;
|
|
6462
|
+
text-align: left;
|
|
6463
|
+
cursor: pointer;
|
|
6464
|
+
box-sizing: border-box;
|
|
6465
|
+
transition: background-color 0.25s ease;
|
|
6466
|
+
}
|
|
6467
|
+
|
|
6468
|
+
.lb-multi-step__step-row:hover {
|
|
6469
|
+
background: color-mix(in srgb, var(--lb-text) 3%, transparent);
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6472
|
+
.lb-multi-step__step-row:focus-visible {
|
|
6473
|
+
outline: 2px solid var(--lb-primary-color);
|
|
6474
|
+
outline-offset: -2px;
|
|
6475
|
+
border-radius: var(--lb-radius-sm);
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
.lb-multi-step__step-row .lb-multi-step__group-count {
|
|
6479
|
+
margin-left: auto;
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
/* Step number \u2014 quiet index, tabular so multi-digit lists align. */
|
|
6483
|
+
.lb-multi-step__step-ix {
|
|
6484
|
+
width: 18px;
|
|
6485
|
+
min-width: 18px;
|
|
6486
|
+
font-size: 12px;
|
|
6487
|
+
font-weight: 500;
|
|
6488
|
+
color: color-mix(in srgb, var(--lb-text) 45%, transparent);
|
|
6489
|
+
font-variant-numeric: tabular-nums;
|
|
6490
|
+
}
|
|
6491
|
+
|
|
6492
|
+
/* The door \u2014 the single wizard entry point, reusing the dashed
|
|
6493
|
+
add-product recipe with a two-line label. JS moves it directly above
|
|
6494
|
+
the first unsatisfied step and hides it once every step is met. The
|
|
6495
|
+
add-product recipe's own margin-top is for its mix & match position
|
|
6496
|
+
(below the slot list); here the pair rules own all spacing. */
|
|
6497
|
+
.lb-multi-step__door {
|
|
6498
|
+
margin-top: 0;
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6501
|
+
.lb-multi-step__door + .lb-multi-step__group {
|
|
6502
|
+
margin-top: 12px;
|
|
6503
|
+
}
|
|
6504
|
+
|
|
6505
|
+
.lb-multi-step__door + .lb-multi-step__group[data-step-mode="row"] {
|
|
6506
|
+
margin-top: 4px;
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
.lb-multi-step__group + .lb-multi-step__door {
|
|
6510
|
+
margin-top: 16px;
|
|
6511
|
+
}
|
|
6512
|
+
|
|
6513
|
+
/* A hidden door is parked as the container's first child (updateDoor), so
|
|
6514
|
+
the group after it is really the first visible element \u2014 no door gap. */
|
|
6515
|
+
.lb-multi-step__door--hidden:first-child + .lb-multi-step__group {
|
|
6516
|
+
margin-top: 0;
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
.lb-multi-step__door-copy {
|
|
6520
|
+
display: flex;
|
|
6521
|
+
flex-direction: column;
|
|
6522
|
+
gap: 2px;
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
.lb-multi-step__door-sub {
|
|
6526
|
+
font-size: 13px;
|
|
6527
|
+
font-weight: 400;
|
|
6528
|
+
line-height: 1.2;
|
|
6529
|
+
color: color-mix(in srgb, var(--lb-text) 55%, transparent);
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6256
6532
|
/* Heading is a quiet, non-interactive row: step name left, live count
|
|
6257
|
-
chip right. The step's
|
|
6533
|
+
chip right. The step's chosen cards below are the edit targets. */
|
|
6258
6534
|
.lb-multi-step__group-heading {
|
|
6259
6535
|
display: flex;
|
|
6260
6536
|
align-items: center;
|
|
@@ -6737,6 +7013,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6737
7013
|
"storefront-token",
|
|
6738
7014
|
"bundle-gid",
|
|
6739
7015
|
"product-handle",
|
|
7016
|
+
"product-id",
|
|
6740
7017
|
"app-url",
|
|
6741
7018
|
"analytics",
|
|
6742
7019
|
"locale",
|
|
@@ -6797,7 +7074,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6797
7074
|
}
|
|
6798
7075
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6799
7076
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6800
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
7077
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6801
7078
|
if (this.shopDomain && this.storefrontToken) {
|
|
6802
7079
|
this.fetchBundle();
|
|
6803
7080
|
}
|
|
@@ -6815,6 +7092,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6815
7092
|
get productHandleAttr() {
|
|
6816
7093
|
return this.getAttribute("product-handle") ?? "";
|
|
6817
7094
|
}
|
|
7095
|
+
/**
|
|
7096
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
7097
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
7098
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
7099
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
7100
|
+
* the bundle's first product.
|
|
7101
|
+
*/
|
|
7102
|
+
get productIdAttr() {
|
|
7103
|
+
return this.getAttribute("product-id") ?? "";
|
|
7104
|
+
}
|
|
6818
7105
|
get appUrl() {
|
|
6819
7106
|
return this.getAttribute("app-url") ?? "";
|
|
6820
7107
|
}
|
|
@@ -6988,7 +7275,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6988
7275
|
});
|
|
6989
7276
|
const storage = window.localStorage;
|
|
6990
7277
|
const key = cartStorageKey(this.shopDomain);
|
|
6991
|
-
|
|
7278
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
6992
7279
|
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
6993
7280
|
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
6994
7281
|
const fail = (message) => {
|
|
@@ -7002,10 +7289,17 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7002
7289
|
};
|
|
7003
7290
|
try {
|
|
7004
7291
|
let checkoutUrl = null;
|
|
7005
|
-
if (
|
|
7292
|
+
if (cartId) {
|
|
7293
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7294
|
+
client,
|
|
7295
|
+
cartId,
|
|
7296
|
+
() => storage?.removeItem(key)
|
|
7297
|
+
);
|
|
7298
|
+
}
|
|
7299
|
+
if (cartId) {
|
|
7006
7300
|
const res = await client.query(
|
|
7007
7301
|
import_core6.CART_LINES_ADD_MUTATION,
|
|
7008
|
-
{ cartId
|
|
7302
|
+
{ cartId, lines }
|
|
7009
7303
|
);
|
|
7010
7304
|
const payload = res.cartLinesAdd;
|
|
7011
7305
|
if (payload?.userErrors?.length) {
|
|
@@ -7058,6 +7352,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7058
7352
|
);
|
|
7059
7353
|
}
|
|
7060
7354
|
}
|
|
7355
|
+
/**
|
|
7356
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7357
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7358
|
+
* clicked add three times while deciding checks out with only what they
|
|
7359
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7360
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7361
|
+
* left alone.
|
|
7362
|
+
*
|
|
7363
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7364
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7365
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7366
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7367
|
+
* checkout matches the shopper's current build.
|
|
7368
|
+
*/
|
|
7369
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7370
|
+
const res = await client.query(import_core6.CART_LINES_QUERY, {
|
|
7371
|
+
cartId
|
|
7372
|
+
});
|
|
7373
|
+
if (!res.cart) {
|
|
7374
|
+
dropSavedCart();
|
|
7375
|
+
return null;
|
|
7376
|
+
}
|
|
7377
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7378
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7379
|
+
).map((line) => line.id);
|
|
7380
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7381
|
+
const removal = await client.query(
|
|
7382
|
+
import_core6.CART_LINES_REMOVE_MUTATION,
|
|
7383
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7384
|
+
);
|
|
7385
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7386
|
+
dropSavedCart();
|
|
7387
|
+
return null;
|
|
7388
|
+
}
|
|
7389
|
+
return cartId;
|
|
7390
|
+
}
|
|
7061
7391
|
reportAddToCartEvent(bundle, lines) {
|
|
7062
7392
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
7063
7393
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -7129,7 +7459,10 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7129
7459
|
);
|
|
7130
7460
|
break;
|
|
7131
7461
|
case "volume":
|
|
7132
|
-
renderVolumeBundle(container, bundle, dispatch
|
|
7462
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7463
|
+
productId: this.productIdAttr || null,
|
|
7464
|
+
productHandle: this.currentProductHandle
|
|
7465
|
+
});
|
|
7133
7466
|
break;
|
|
7134
7467
|
case "bogo":
|
|
7135
7468
|
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|