@lime-bundles/widget 4.1.1 → 4.3.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 +4 -1
- package/dist/index.cjs +265 -100
- package/dist/index.d.cts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +273 -104
- package/dist/lime-bundle.global.js +85 -33
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
6
6
|
CART_CREATE_MUTATION,
|
|
7
7
|
CART_LINES_ADD_MUTATION,
|
|
8
|
+
CART_LINES_QUERY,
|
|
9
|
+
CART_LINES_REMOVE_MUTATION,
|
|
8
10
|
SHOP_SETTINGS_QUERY,
|
|
9
11
|
parseMetaobjectBundle,
|
|
10
12
|
observeImpression,
|
|
@@ -15,17 +17,38 @@ import {
|
|
|
15
17
|
hasInContext,
|
|
16
18
|
withInContext,
|
|
17
19
|
isVisibleInMarket,
|
|
18
|
-
parseOutOfStockBehavior,
|
|
19
|
-
DEFAULT_OUT_OF_STOCK_BEHAVIOR,
|
|
20
20
|
getVisitorId,
|
|
21
21
|
resolveAbTests
|
|
22
22
|
} from "@lime-bundles/core";
|
|
23
23
|
|
|
24
|
+
// ../render/src/host.ts
|
|
25
|
+
function intlFormatMoney(currencyCode) {
|
|
26
|
+
return (cents) => {
|
|
27
|
+
try {
|
|
28
|
+
return new Intl.NumberFormat(void 0, {
|
|
29
|
+
style: "currency",
|
|
30
|
+
currency: currencyCode
|
|
31
|
+
}).format(cents / 100);
|
|
32
|
+
} catch {
|
|
33
|
+
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
38
|
+
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
39
|
+
function bundleLineAttributes(bundleGid, bundleType) {
|
|
40
|
+
return [
|
|
41
|
+
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
42
|
+
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
// ../render/src/adapt.ts
|
|
25
47
|
import {
|
|
26
48
|
formatUnitPrice,
|
|
27
49
|
isVariantFulfillable,
|
|
28
|
-
resolveBundleQty
|
|
50
|
+
resolveBundleQty,
|
|
51
|
+
stockCapForVariant
|
|
29
52
|
} from "@lime-bundles/core";
|
|
30
53
|
function numericId(gid) {
|
|
31
54
|
const tail = gid.slice(gid.lastIndexOf("/") + 1);
|
|
@@ -49,7 +72,10 @@ function adaptVariant(v, requiredQty, fallbackCurrency) {
|
|
|
49
72
|
price: toCents(v.price?.amount),
|
|
50
73
|
compareAtPrice: toCents(v.compareAtPrice?.amount),
|
|
51
74
|
unitPrice: formatUnitPrice(v.unitPrice, v.unitPriceMeasurement, fallbackCurrency),
|
|
52
|
-
image: v.image?.url ?? null
|
|
75
|
+
image: v.image?.url ?? null,
|
|
76
|
+
// Carried so the picker's stepper cap and cross-slot stock guard can
|
|
77
|
+
// bind on the headless surface; dropping it here left them capless.
|
|
78
|
+
inventoryQuantity: stockCapForVariant(v)
|
|
53
79
|
};
|
|
54
80
|
}
|
|
55
81
|
function adaptProduct(product, bundleId, opts) {
|
|
@@ -104,6 +130,14 @@ function findVariant(product, variantId) {
|
|
|
104
130
|
}
|
|
105
131
|
return product.variants[0];
|
|
106
132
|
}
|
|
133
|
+
function resolveSellableVariant(product) {
|
|
134
|
+
const seeded = findVariant(product, product.selectedVariantId);
|
|
135
|
+
if (seeded && seeded.available !== false) return seeded;
|
|
136
|
+
for (let i = 0; i < product.variants.length; i++) {
|
|
137
|
+
if (product.variants[i].available !== false) return product.variants[i];
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
107
141
|
function findVariantByOptions(product, optionValues) {
|
|
108
142
|
if (!product.variants || !optionValues) return null;
|
|
109
143
|
for (let i = 0; i < product.variants.length; i++) {
|
|
@@ -216,28 +250,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
216
250
|
}
|
|
217
251
|
}
|
|
218
252
|
|
|
219
|
-
// ../render/src/host.ts
|
|
220
|
-
function intlFormatMoney(currencyCode) {
|
|
221
|
-
return (cents) => {
|
|
222
|
-
try {
|
|
223
|
-
return new Intl.NumberFormat(void 0, {
|
|
224
|
-
style: "currency",
|
|
225
|
-
currency: currencyCode
|
|
226
|
-
}).format(cents / 100);
|
|
227
|
-
} catch {
|
|
228
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
233
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
234
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
235
|
-
return [
|
|
236
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
237
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
238
|
-
];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
253
|
// ../render/src/product/option-selects.ts
|
|
242
254
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
243
255
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -1174,12 +1186,11 @@ function bindDropdowns(root) {
|
|
|
1174
1186
|
}
|
|
1175
1187
|
|
|
1176
1188
|
// src/renderers/fixed.ts
|
|
1177
|
-
function renderFixedBundle(container, bundle,
|
|
1189
|
+
function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
1178
1190
|
const wc = bundle.widgetConfig;
|
|
1179
1191
|
const products = adaptProducts(bundle);
|
|
1180
1192
|
if (!products.length) return;
|
|
1181
1193
|
const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));
|
|
1182
|
-
if (outOfStockBehavior === "hide" && oosProducts.length > 0) return;
|
|
1183
1194
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1184
1195
|
const formatMoney = intlFormatMoney(currency);
|
|
1185
1196
|
const t = DEFAULT_STRINGS;
|
|
@@ -1204,7 +1215,9 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
|
|
|
1204
1215
|
});
|
|
1205
1216
|
shell.products.appendChild(row);
|
|
1206
1217
|
if (isOos) continue;
|
|
1207
|
-
const
|
|
1218
|
+
const sellable = resolveSellableVariant(product);
|
|
1219
|
+
if (sellable) product.selectedVariantId = sellable.id;
|
|
1220
|
+
const selected = sellable ?? findVariant(product, product.selectedVariantId);
|
|
1208
1221
|
hydrateRowControls(row, product, selected);
|
|
1209
1222
|
applyVariant(row, selected, product, formatMoney);
|
|
1210
1223
|
bindVariantSelects(
|
|
@@ -1799,6 +1812,17 @@ function unlock() {
|
|
|
1799
1812
|
window.scrollTo(0, savedY);
|
|
1800
1813
|
}
|
|
1801
1814
|
|
|
1815
|
+
// ../render/src/picker/sort.ts
|
|
1816
|
+
function sinkAddedRows(list, rowEls, isAdded) {
|
|
1817
|
+
const front = [];
|
|
1818
|
+
const back = [];
|
|
1819
|
+
for (let i = 0; i < rowEls.length; i++) {
|
|
1820
|
+
(isAdded(i) ? back : front).push(rowEls[i]);
|
|
1821
|
+
}
|
|
1822
|
+
for (const el2 of front) list.appendChild(el2);
|
|
1823
|
+
for (const el2 of back) list.appendChild(el2);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1802
1826
|
// ../render/src/mix-match/modal.ts
|
|
1803
1827
|
var ALL_TYPES = "__all__";
|
|
1804
1828
|
var CLOSE_TRANSITION_MS = 300;
|
|
@@ -2023,12 +2047,22 @@ function createPickerModal(deps) {
|
|
|
2023
2047
|
syncActiveFilterPill();
|
|
2024
2048
|
if (searchInput) searchInput.value = "";
|
|
2025
2049
|
filterProducts(searchInput ? searchInput.value : "");
|
|
2050
|
+
if (modalList) {
|
|
2051
|
+
const selectedProductIds = /* @__PURE__ */ Object.create(null);
|
|
2052
|
+
for (const it of items) selectedProductIds[String(it.productId)] = true;
|
|
2053
|
+
sinkAddedRows(
|
|
2054
|
+
modalList,
|
|
2055
|
+
rows.map((r) => r.el),
|
|
2056
|
+
(i) => selectedProductIds[String(eligibleProducts[i].id)] === true
|
|
2057
|
+
);
|
|
2058
|
+
modalList.scrollTop = 0;
|
|
2059
|
+
}
|
|
2026
2060
|
overlay.style.display = "";
|
|
2027
2061
|
void overlay.offsetHeight;
|
|
2028
2062
|
overlay.classList.add("lb-mix-match__modal-overlay--open");
|
|
2029
2063
|
lock();
|
|
2030
2064
|
setTimeout(() => {
|
|
2031
|
-
if (
|
|
2065
|
+
if (closeBtn) closeBtn.focus();
|
|
2032
2066
|
else modalDialog?.focus();
|
|
2033
2067
|
}, 50);
|
|
2034
2068
|
}
|
|
@@ -2585,7 +2619,7 @@ function buildPickerModal(opts) {
|
|
|
2585
2619
|
}
|
|
2586
2620
|
|
|
2587
2621
|
// src/renderers/mix-match.ts
|
|
2588
|
-
function renderMixMatchBundle(container, bundle,
|
|
2622
|
+
function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, currentProductHandle) {
|
|
2589
2623
|
const wc = bundle.widgetConfig;
|
|
2590
2624
|
const t = DEFAULT_STRINGS;
|
|
2591
2625
|
const requiredQty = bundle.minQuantity || 1;
|
|
@@ -2608,12 +2642,12 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2608
2642
|
compareAtPrice: v.compareAtPrice,
|
|
2609
2643
|
unitPrice: v.unitPrice,
|
|
2610
2644
|
image: v.image,
|
|
2611
|
-
inventoryQuantity: null
|
|
2645
|
+
inventoryQuantity: v.inventoryQuantity ?? null
|
|
2612
2646
|
}))
|
|
2613
2647
|
}));
|
|
2614
2648
|
if (!eligibleProducts.length) return;
|
|
2615
2649
|
const poolUnits = eligibleProducts.filter((p) => p.available).length;
|
|
2616
|
-
if (
|
|
2650
|
+
if (poolUnits === 0) return;
|
|
2617
2651
|
const formatMoney = intlFormatMoney(
|
|
2618
2652
|
bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
|
|
2619
2653
|
);
|
|
@@ -2776,10 +2810,51 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2776
2810
|
// src/renderers/volume.ts
|
|
2777
2811
|
import {
|
|
2778
2812
|
bestValueTierIndex,
|
|
2813
|
+
isVariantFulfillable as isVariantFulfillable2,
|
|
2814
|
+
resolveContextProduct,
|
|
2779
2815
|
resolveDefaultTierIndex,
|
|
2780
|
-
resolvePopularTierIndex
|
|
2816
|
+
resolvePopularTierIndex,
|
|
2817
|
+
stockCapForVariant as stockCapForVariant2
|
|
2781
2818
|
} from "@lime-bundles/core";
|
|
2782
2819
|
|
|
2820
|
+
// ../render/src/volume/stock.ts
|
|
2821
|
+
var TIER_OOS_CLASS = "lb-volume__tier--oos";
|
|
2822
|
+
function tierQty(el2) {
|
|
2823
|
+
return parseInt(el2.getAttribute("data-tier-qty") ?? "", 10) || 0;
|
|
2824
|
+
}
|
|
2825
|
+
function tierUnavailable(el2) {
|
|
2826
|
+
return el2.getAttribute("aria-disabled") === "true";
|
|
2827
|
+
}
|
|
2828
|
+
function applyTierStockState(tierEls, cap) {
|
|
2829
|
+
for (let i = 0; i < tierEls.length; i++) {
|
|
2830
|
+
const el2 = tierEls[i];
|
|
2831
|
+
const unavailable = cap !== null && tierQty(el2) > cap;
|
|
2832
|
+
if (unavailable) {
|
|
2833
|
+
el2.classList.add(TIER_OOS_CLASS);
|
|
2834
|
+
el2.setAttribute("aria-disabled", "true");
|
|
2835
|
+
} else {
|
|
2836
|
+
el2.classList.remove(TIER_OOS_CLASS);
|
|
2837
|
+
el2.removeAttribute("aria-disabled");
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
function resolveAvailableTierIndex(tierEls, preferred) {
|
|
2842
|
+
const p = tierEls[preferred];
|
|
2843
|
+
if (p && !tierUnavailable(p)) return preferred;
|
|
2844
|
+
for (let i = 0; i < tierEls.length; i++) {
|
|
2845
|
+
if (!tierUnavailable(tierEls[i])) return i;
|
|
2846
|
+
}
|
|
2847
|
+
return -1;
|
|
2848
|
+
}
|
|
2849
|
+
function stepToAvailableTier(tierEls, from, direction) {
|
|
2850
|
+
let i = from + direction;
|
|
2851
|
+
while (i >= 0 && i < tierEls.length) {
|
|
2852
|
+
if (!tierUnavailable(tierEls[i])) return i;
|
|
2853
|
+
i += direction;
|
|
2854
|
+
}
|
|
2855
|
+
return from;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2783
2858
|
// ../render/src/volume/pricing.ts
|
|
2784
2859
|
function calcTierPrice(basePrice, discountType, tierEl) {
|
|
2785
2860
|
if (discountType === "fixed_amount") {
|
|
@@ -2869,15 +2944,16 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2869
2944
|
}
|
|
2870
2945
|
|
|
2871
2946
|
// src/renderers/volume.ts
|
|
2872
|
-
function renderVolumeBundle(container, bundle,
|
|
2947
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2873
2948
|
const wc = bundle.widgetConfig;
|
|
2874
|
-
const product = bundle.products
|
|
2949
|
+
const product = resolveContextProduct(bundle.products, productContext);
|
|
2875
2950
|
if (!product) return;
|
|
2876
2951
|
const variants = product.variants.nodes;
|
|
2877
|
-
const
|
|
2952
|
+
const minTierQty = Math.min(
|
|
2953
|
+
...bundle.volumeTiers.map((tier) => tier.minQuantity)
|
|
2954
|
+
);
|
|
2955
|
+
const pricingVariant = variants.find((v) => isVariantFulfillable2(v, minTierQty)) ?? variants.find((v) => v.availableForSale) ?? variants[0];
|
|
2878
2956
|
if (!pricingVariant) return;
|
|
2879
|
-
const anyAvailable = variants.some((v) => v.availableForSale);
|
|
2880
|
-
if (outOfStockBehavior === "hide" && !anyAvailable) return;
|
|
2881
2957
|
const basePrice = toCents(pricingVariant.price.amount);
|
|
2882
2958
|
const discountType = bundle.discountConfig.discountType;
|
|
2883
2959
|
const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
|
|
@@ -2928,28 +3004,31 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
|
|
|
2928
3004
|
shell.products.replaceWith(group);
|
|
2929
3005
|
const tierEls = group.querySelectorAll("[data-tier-index]");
|
|
2930
3006
|
updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
|
|
2931
|
-
|
|
3007
|
+
applyTierStockState(tierEls, stockCapForVariant2(pricingVariant));
|
|
3008
|
+
let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);
|
|
3009
|
+
const noTierAvailable = selectedIndex === -1;
|
|
3010
|
+
if (noTierAvailable) selectedIndex = defaultIdx;
|
|
2932
3011
|
const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
|
|
2933
3012
|
group.addEventListener("click", (e) => {
|
|
2934
3013
|
const el2 = e.target.closest("[data-tier-index]");
|
|
2935
|
-
if (!el2) return;
|
|
3014
|
+
if (!el2 || tierUnavailable(el2)) return;
|
|
2936
3015
|
selectedIndex = parseInt(el2.getAttribute("data-tier-index") ?? "", 10) || 0;
|
|
2937
3016
|
apply();
|
|
2938
3017
|
});
|
|
2939
3018
|
group.addEventListener("keydown", (e) => {
|
|
2940
3019
|
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
|
2941
3020
|
e.preventDefault();
|
|
2942
|
-
selectedIndex =
|
|
3021
|
+
selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);
|
|
2943
3022
|
apply();
|
|
2944
3023
|
tierEls[selectedIndex].focus();
|
|
2945
3024
|
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
2946
3025
|
e.preventDefault();
|
|
2947
|
-
selectedIndex =
|
|
3026
|
+
selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);
|
|
2948
3027
|
apply();
|
|
2949
3028
|
tierEls[selectedIndex].focus();
|
|
2950
3029
|
}
|
|
2951
3030
|
});
|
|
2952
|
-
if (
|
|
3031
|
+
if (noTierAvailable) {
|
|
2953
3032
|
shell.cta.disabled = true;
|
|
2954
3033
|
const label = shell.cta.querySelector("[data-cta-label]");
|
|
2955
3034
|
if (label) label.textContent = t.outOfStock || "Out of stock";
|
|
@@ -3084,7 +3163,7 @@ function buildBogoPlus() {
|
|
|
3084
3163
|
plus.innerHTML = '<svg width="12" height="12" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="9" y1="3" x2="9" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="9" x2="15" y2="9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
3085
3164
|
return plus;
|
|
3086
3165
|
}
|
|
3087
|
-
function renderBogoBundle(container, bundle,
|
|
3166
|
+
function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3088
3167
|
const wc = bundle.widgetConfig;
|
|
3089
3168
|
const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
|
|
3090
3169
|
const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);
|
|
@@ -3113,7 +3192,6 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
|
|
|
3113
3192
|
}
|
|
3114
3193
|
];
|
|
3115
3194
|
const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;
|
|
3116
|
-
if (outOfStockBehavior === "hide" && oosCount > 0) return;
|
|
3117
3195
|
const formatMoney = intlFormatMoney(
|
|
3118
3196
|
buyProduct.priceRange.minVariantPrice.currencyCode
|
|
3119
3197
|
);
|
|
@@ -3549,6 +3627,14 @@ function createWizard(deps) {
|
|
|
3549
3627
|
rows.push(row);
|
|
3550
3628
|
modalList.appendChild(row.el);
|
|
3551
3629
|
}
|
|
3630
|
+
const stepPicks = selections[forStep] || [];
|
|
3631
|
+
const selectedProductIds = /* @__PURE__ */ Object.create(null);
|
|
3632
|
+
for (const it of stepPicks) selectedProductIds[String(it.productId)] = true;
|
|
3633
|
+
sinkAddedRows(
|
|
3634
|
+
modalList,
|
|
3635
|
+
rows.map((r) => r.el),
|
|
3636
|
+
(i) => selectedProductIds[String(pool[i].id)] === true
|
|
3637
|
+
);
|
|
3552
3638
|
deps.dropdown?.bindAll(modalList);
|
|
3553
3639
|
}
|
|
3554
3640
|
function onListClick(e) {
|
|
@@ -3711,6 +3797,7 @@ function createWizard(deps) {
|
|
|
3711
3797
|
syncActiveFilterPill();
|
|
3712
3798
|
if (searchInput) searchInput.value = "";
|
|
3713
3799
|
filterProducts("");
|
|
3800
|
+
if (modalList) modalList.scrollTop = 0;
|
|
3714
3801
|
buildStepSegments(stepIndex);
|
|
3715
3802
|
refresh();
|
|
3716
3803
|
if (modalLive) {
|
|
@@ -3734,7 +3821,7 @@ function createWizard(deps) {
|
|
|
3734
3821
|
overlay.classList.add("lb-mix-match__modal-overlay--open");
|
|
3735
3822
|
lock();
|
|
3736
3823
|
setTimeout(() => {
|
|
3737
|
-
if (
|
|
3824
|
+
if (closeBtn) closeBtn.focus();
|
|
3738
3825
|
else modalDialog?.focus();
|
|
3739
3826
|
}, 50);
|
|
3740
3827
|
}
|
|
@@ -3802,7 +3889,7 @@ function createWizard(deps) {
|
|
|
3802
3889
|
}
|
|
3803
3890
|
|
|
3804
3891
|
// src/renderers/multi-step.ts
|
|
3805
|
-
function renderMultiStepBundle(container, bundle,
|
|
3892
|
+
function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3806
3893
|
const wc = bundle.widgetConfig;
|
|
3807
3894
|
const t = DEFAULT_STRINGS;
|
|
3808
3895
|
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
@@ -3835,13 +3922,13 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
|
|
|
3835
3922
|
compareAtPrice: v.compareAtPrice,
|
|
3836
3923
|
unitPrice: v.unitPrice,
|
|
3837
3924
|
image: v.image,
|
|
3838
|
-
inventoryQuantity: null
|
|
3925
|
+
inventoryQuantity: v.inventoryQuantity ?? null
|
|
3839
3926
|
}))
|
|
3840
3927
|
};
|
|
3841
3928
|
})
|
|
3842
3929
|
);
|
|
3843
|
-
const
|
|
3844
|
-
if (
|
|
3930
|
+
const everyStepPickable = pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));
|
|
3931
|
+
if (!everyStepPickable) return;
|
|
3845
3932
|
const formatMoney = intlFormatMoney(
|
|
3846
3933
|
bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
|
|
3847
3934
|
);
|
|
@@ -5653,7 +5740,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5653
5740
|
transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
|
|
5654
5741
|
}
|
|
5655
5742
|
|
|
5656
|
-
.lb-mix-match__modal-add:hover {
|
|
5743
|
+
.lb-mix-match__modal-add:hover:not(:disabled) {
|
|
5657
5744
|
opacity: 0.9;
|
|
5658
5745
|
}
|
|
5659
5746
|
|
|
@@ -5768,8 +5855,15 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5768
5855
|
color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);
|
|
5769
5856
|
}
|
|
5770
5857
|
|
|
5771
|
-
/* "Done" reuses the picker Add-button styling so it matches the Add buttons.
|
|
5858
|
+
/* "Done" reuses the picker Add-button styling so it matches the Add buttons.
|
|
5859
|
+
The flex centring + min-height pin the footer-button silhouette: the wizard
|
|
5860
|
+
Back (bundle-multi-step.css) copies these metrics so the pair stays
|
|
5861
|
+
equal-height whatever border widths the merchant's picker tokens set. */
|
|
5772
5862
|
.lb-mix-match__modal-done {
|
|
5863
|
+
display: inline-flex;
|
|
5864
|
+
align-items: center;
|
|
5865
|
+
justify-content: center;
|
|
5866
|
+
min-height: 40px;
|
|
5773
5867
|
padding: 10px 24px;
|
|
5774
5868
|
transition: opacity 0.25s ease;
|
|
5775
5869
|
background: var(--lb-picker-add-bg);
|
|
@@ -5783,7 +5877,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5783
5877
|
cursor: pointer;
|
|
5784
5878
|
}
|
|
5785
5879
|
|
|
5786
|
-
.lb-mix-match__modal-done:hover {
|
|
5880
|
+
.lb-mix-match__modal-done:hover:not(:disabled) {
|
|
5787
5881
|
opacity: 0.9;
|
|
5788
5882
|
}
|
|
5789
5883
|
|
|
@@ -5793,6 +5887,16 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5793
5887
|
box-shadow: none;
|
|
5794
5888
|
}
|
|
5795
5889
|
|
|
5890
|
+
/* Disabled Next/Done (wizard step minimum not met) \u2014 same washed-out
|
|
5891
|
+
treatment as a disabled row Add, so an unfinished step reads at a
|
|
5892
|
+
glance. Hover is gated off above; the solid button returns the moment
|
|
5893
|
+
the requirement is met. */
|
|
5894
|
+
.lb-mix-match__modal-done:disabled {
|
|
5895
|
+
background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));
|
|
5896
|
+
color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);
|
|
5897
|
+
cursor: not-allowed;
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5796
5900
|
/* Hidden utility for search filtering */
|
|
5797
5901
|
.lb-hidden {
|
|
5798
5902
|
display: none !important;
|
|
@@ -6049,6 +6153,13 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
|
|
|
6049
6153
|
white-space: nowrap;
|
|
6050
6154
|
font-variant-numeric: tabular-nums;
|
|
6051
6155
|
}
|
|
6156
|
+
|
|
6157
|
+
/* Tier the selected variant can't cover \u2014 same greyed treatment as an
|
|
6158
|
+
out-of-stock product row, and not selectable. */
|
|
6159
|
+
.lb-volume__tier--oos {
|
|
6160
|
+
opacity: 0.5;
|
|
6161
|
+
cursor: not-allowed;
|
|
6162
|
+
}
|
|
6052
6163
|
`;
|
|
6053
6164
|
var BUNDLE_BOGO_CSS = `/* Lime Bundles \u2014 BOGO (Buy X Get Y) bundle styles */
|
|
6054
6165
|
|
|
@@ -6217,14 +6328,23 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6217
6328
|
justify-self: end;
|
|
6218
6329
|
}
|
|
6219
6330
|
|
|
6220
|
-
/* Back \u2014 secondary treatment beside the primary Next/Done
|
|
6221
|
-
.lb-mix-match__modal-done).
|
|
6331
|
+
/* Back \u2014 secondary (outlined) treatment beside the primary Next/Done
|
|
6332
|
+
(which reuse .lb-mix-match__modal-done). Same silhouette as Next \u2014
|
|
6333
|
+
metrics copied from the modal-done recipe (padding, type, radius,
|
|
6334
|
+
min-height) \u2014 and picker tokens only: the modal is portaled out of the
|
|
6335
|
+
widget, so the widget vars (--lb-text, --lb-border-*) it previously
|
|
6336
|
+
used don't reliably resolve here and break palette isolation. */
|
|
6222
6337
|
.lb-multi-step__modal-back {
|
|
6223
|
-
|
|
6338
|
+
display: inline-flex;
|
|
6339
|
+
align-items: center;
|
|
6340
|
+
justify-content: center;
|
|
6341
|
+
min-height: 40px;
|
|
6342
|
+
padding: 10px 24px;
|
|
6224
6343
|
background: none;
|
|
6225
|
-
border: var(--lb-border-width) solid var(--lb-border-color);
|
|
6226
|
-
border-radius: var(--lb-radius-sm);
|
|
6227
|
-
|
|
6344
|
+
border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
|
|
6345
|
+
border-radius: var(--lb-picker-radius-sm);
|
|
6346
|
+
box-sizing: border-box;
|
|
6347
|
+
color: var(--lb-picker-text);
|
|
6228
6348
|
font-family: inherit;
|
|
6229
6349
|
font-size: 14px;
|
|
6230
6350
|
font-weight: 600;
|
|
@@ -6233,7 +6353,7 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6233
6353
|
}
|
|
6234
6354
|
|
|
6235
6355
|
.lb-multi-step__modal-back:hover {
|
|
6236
|
-
background: color-mix(in srgb, var(--lb-text) 5%, transparent);
|
|
6356
|
+
background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);
|
|
6237
6357
|
}
|
|
6238
6358
|
|
|
6239
6359
|
.lb-multi-step__modal-back:focus-visible {
|
|
@@ -6625,6 +6745,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6625
6745
|
"storefront-token",
|
|
6626
6746
|
"bundle-gid",
|
|
6627
6747
|
"product-handle",
|
|
6748
|
+
"product-id",
|
|
6628
6749
|
"app-url",
|
|
6629
6750
|
"analytics",
|
|
6630
6751
|
"locale",
|
|
@@ -6663,12 +6784,6 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6663
6784
|
* `.lb-bundle-widget { ... }` reach the widget's DOM.
|
|
6664
6785
|
*/
|
|
6665
6786
|
shopCustomCss = null;
|
|
6666
|
-
/**
|
|
6667
|
-
* Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
|
|
6668
|
-
* before the first render. Defaults to `show_greyed_out` so a failed or
|
|
6669
|
-
* missing fetch degrades to "bundles still render".
|
|
6670
|
-
*/
|
|
6671
|
-
outOfStockBehavior = DEFAULT_OUT_OF_STOCK_BEHAVIOR;
|
|
6672
6787
|
constructor() {
|
|
6673
6788
|
super();
|
|
6674
6789
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
@@ -6691,7 +6806,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6691
6806
|
}
|
|
6692
6807
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6693
6808
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6694
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
6809
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6695
6810
|
if (this.shopDomain && this.storefrontToken) {
|
|
6696
6811
|
this.fetchBundle();
|
|
6697
6812
|
}
|
|
@@ -6709,6 +6824,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6709
6824
|
get productHandleAttr() {
|
|
6710
6825
|
return this.getAttribute("product-handle") ?? "";
|
|
6711
6826
|
}
|
|
6827
|
+
/**
|
|
6828
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
6829
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
6830
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
6831
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
6832
|
+
* the bundle's first product.
|
|
6833
|
+
*/
|
|
6834
|
+
get productIdAttr() {
|
|
6835
|
+
return this.getAttribute("product-id") ?? "";
|
|
6836
|
+
}
|
|
6712
6837
|
get appUrl() {
|
|
6713
6838
|
return this.getAttribute("app-url") ?? "";
|
|
6714
6839
|
}
|
|
@@ -6804,9 +6929,6 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6804
6929
|
const sanitized = sanitizeCustomCss(customCss);
|
|
6805
6930
|
if (sanitized.ok) this.shopCustomCss = sanitized.css;
|
|
6806
6931
|
}
|
|
6807
|
-
this.outOfStockBehavior = parseOutOfStockBehavior(
|
|
6808
|
-
shopSettings?.shop?.outOfStockBehavior?.value
|
|
6809
|
-
);
|
|
6810
6932
|
this.renderBundles();
|
|
6811
6933
|
} catch (err) {
|
|
6812
6934
|
if (controller.signal.aborted) return;
|
|
@@ -6885,17 +7007,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6885
7007
|
});
|
|
6886
7008
|
const storage = window.localStorage;
|
|
6887
7009
|
const key = cartStorageKey(this.shopDomain);
|
|
6888
|
-
|
|
7010
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
7011
|
+
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
7012
|
+
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
7013
|
+
const fail = (message) => {
|
|
7014
|
+
this.dispatchEvent(
|
|
7015
|
+
new CustomEvent("lime-bundle:error", {
|
|
7016
|
+
detail: { message, code: "CART_ERROR" },
|
|
7017
|
+
bubbles: true,
|
|
7018
|
+
composed: true
|
|
7019
|
+
})
|
|
7020
|
+
);
|
|
7021
|
+
};
|
|
6889
7022
|
try {
|
|
6890
7023
|
let checkoutUrl = null;
|
|
6891
|
-
if (
|
|
7024
|
+
if (cartId) {
|
|
7025
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7026
|
+
client,
|
|
7027
|
+
cartId,
|
|
7028
|
+
() => storage?.removeItem(key)
|
|
7029
|
+
);
|
|
7030
|
+
}
|
|
7031
|
+
if (cartId) {
|
|
6892
7032
|
const res = await client.query(
|
|
6893
7033
|
CART_LINES_ADD_MUTATION,
|
|
6894
|
-
{ cartId
|
|
7034
|
+
{ cartId, lines }
|
|
6895
7035
|
);
|
|
6896
7036
|
const payload = res.cartLinesAdd;
|
|
6897
7037
|
if (payload?.userErrors?.length) {
|
|
7038
|
+
if (!isStaleCartError(payload.userErrors)) {
|
|
7039
|
+
fail(payload.userErrors[0].message);
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
6898
7042
|
storage?.removeItem(key);
|
|
7043
|
+
} else if (stockCapped(payload?.warnings)) {
|
|
7044
|
+
fail("Some items in this bundle are out of stock.");
|
|
7045
|
+
return;
|
|
6899
7046
|
} else if (payload?.cart) {
|
|
6900
7047
|
checkoutUrl = payload.cart.checkoutUrl;
|
|
6901
7048
|
}
|
|
@@ -6906,6 +7053,14 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6906
7053
|
{ input: { lines } }
|
|
6907
7054
|
);
|
|
6908
7055
|
const payload = res.cartCreate;
|
|
7056
|
+
if (payload?.userErrors?.length) {
|
|
7057
|
+
fail(payload.userErrors[0].message);
|
|
7058
|
+
return;
|
|
7059
|
+
}
|
|
7060
|
+
if (stockCapped(payload?.warnings)) {
|
|
7061
|
+
fail("Some items in this bundle are out of stock.");
|
|
7062
|
+
return;
|
|
7063
|
+
}
|
|
6909
7064
|
if (payload?.cart) {
|
|
6910
7065
|
storage?.setItem(key, payload.cart.id);
|
|
6911
7066
|
checkoutUrl = payload.cart.checkoutUrl;
|
|
@@ -6914,13 +7069,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6914
7069
|
if (checkoutUrl) {
|
|
6915
7070
|
window.location.assign(checkoutUrl);
|
|
6916
7071
|
} else {
|
|
6917
|
-
|
|
6918
|
-
new CustomEvent("lime-bundle:error", {
|
|
6919
|
-
detail: { message: "Cart creation failed", code: "CART_ERROR" },
|
|
6920
|
-
bubbles: true,
|
|
6921
|
-
composed: true
|
|
6922
|
-
})
|
|
6923
|
-
);
|
|
7072
|
+
fail("Cart creation failed");
|
|
6924
7073
|
}
|
|
6925
7074
|
} catch (err) {
|
|
6926
7075
|
this.dispatchEvent(
|
|
@@ -6935,6 +7084,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6935
7084
|
);
|
|
6936
7085
|
}
|
|
6937
7086
|
}
|
|
7087
|
+
/**
|
|
7088
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7089
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7090
|
+
* clicked add three times while deciding checks out with only what they
|
|
7091
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7092
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7093
|
+
* left alone.
|
|
7094
|
+
*
|
|
7095
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7096
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7097
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7098
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7099
|
+
* checkout matches the shopper's current build.
|
|
7100
|
+
*/
|
|
7101
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7102
|
+
const res = await client.query(CART_LINES_QUERY, {
|
|
7103
|
+
cartId
|
|
7104
|
+
});
|
|
7105
|
+
if (!res.cart) {
|
|
7106
|
+
dropSavedCart();
|
|
7107
|
+
return null;
|
|
7108
|
+
}
|
|
7109
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7110
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7111
|
+
).map((line) => line.id);
|
|
7112
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7113
|
+
const removal = await client.query(
|
|
7114
|
+
CART_LINES_REMOVE_MUTATION,
|
|
7115
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7116
|
+
);
|
|
7117
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7118
|
+
dropSavedCart();
|
|
7119
|
+
return null;
|
|
7120
|
+
}
|
|
7121
|
+
return cartId;
|
|
7122
|
+
}
|
|
6938
7123
|
reportAddToCartEvent(bundle, lines) {
|
|
6939
7124
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
6940
7125
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -6994,44 +7179,28 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6994
7179
|
const registerCleanup = (fn) => this.renderCleanups.push(fn);
|
|
6995
7180
|
switch (bundle.bundleType) {
|
|
6996
7181
|
case "fixed":
|
|
6997
|
-
renderFixedBundle(
|
|
6998
|
-
container,
|
|
6999
|
-
bundle,
|
|
7000
|
-
this.outOfStockBehavior,
|
|
7001
|
-
dispatch,
|
|
7002
|
-
registerCleanup
|
|
7003
|
-
);
|
|
7182
|
+
renderFixedBundle(container, bundle, dispatch, registerCleanup);
|
|
7004
7183
|
break;
|
|
7005
7184
|
case "mix_match":
|
|
7006
7185
|
renderMixMatchBundle(
|
|
7007
7186
|
container,
|
|
7008
7187
|
bundle,
|
|
7009
|
-
this.outOfStockBehavior,
|
|
7010
7188
|
dispatch,
|
|
7011
7189
|
registerCleanup,
|
|
7012
7190
|
this.currentProductHandle
|
|
7013
7191
|
);
|
|
7014
7192
|
break;
|
|
7015
7193
|
case "volume":
|
|
7016
|
-
renderVolumeBundle(container, bundle,
|
|
7194
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7195
|
+
productId: this.productIdAttr || null,
|
|
7196
|
+
productHandle: this.currentProductHandle
|
|
7197
|
+
});
|
|
7017
7198
|
break;
|
|
7018
7199
|
case "bogo":
|
|
7019
|
-
renderBogoBundle(
|
|
7020
|
-
container,
|
|
7021
|
-
bundle,
|
|
7022
|
-
this.outOfStockBehavior,
|
|
7023
|
-
dispatch,
|
|
7024
|
-
registerCleanup
|
|
7025
|
-
);
|
|
7200
|
+
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|
|
7026
7201
|
break;
|
|
7027
7202
|
case "multi_step":
|
|
7028
|
-
renderMultiStepBundle(
|
|
7029
|
-
container,
|
|
7030
|
-
bundle,
|
|
7031
|
-
this.outOfStockBehavior,
|
|
7032
|
-
dispatch,
|
|
7033
|
-
registerCleanup
|
|
7034
|
-
);
|
|
7203
|
+
renderMultiStepBundle(container, bundle, dispatch, registerCleanup);
|
|
7035
7204
|
break;
|
|
7036
7205
|
}
|
|
7037
7206
|
this.shadow.appendChild(container);
|