@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.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) {
|
|
@@ -52,7 +74,10 @@ function adaptVariant(v, requiredQty, fallbackCurrency) {
|
|
|
52
74
|
price: toCents(v.price?.amount),
|
|
53
75
|
compareAtPrice: toCents(v.compareAtPrice?.amount),
|
|
54
76
|
unitPrice: (0, import_core.formatUnitPrice)(v.unitPrice, v.unitPriceMeasurement, fallbackCurrency),
|
|
55
|
-
image: v.image?.url ?? null
|
|
77
|
+
image: v.image?.url ?? null,
|
|
78
|
+
// Carried so the picker's stepper cap and cross-slot stock guard can
|
|
79
|
+
// bind on the headless surface; dropping it here left them capless.
|
|
80
|
+
inventoryQuantity: (0, import_core.stockCapForVariant)(v)
|
|
56
81
|
};
|
|
57
82
|
}
|
|
58
83
|
function adaptProduct(product, bundleId, opts) {
|
|
@@ -107,6 +132,14 @@ function findVariant(product, variantId) {
|
|
|
107
132
|
}
|
|
108
133
|
return product.variants[0];
|
|
109
134
|
}
|
|
135
|
+
function resolveSellableVariant(product) {
|
|
136
|
+
const seeded = findVariant(product, product.selectedVariantId);
|
|
137
|
+
if (seeded && seeded.available !== false) return seeded;
|
|
138
|
+
for (let i = 0; i < product.variants.length; i++) {
|
|
139
|
+
if (product.variants[i].available !== false) return product.variants[i];
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
110
143
|
function findVariantByOptions(product, optionValues) {
|
|
111
144
|
if (!product.variants || !optionValues) return null;
|
|
112
145
|
for (let i = 0; i < product.variants.length; i++) {
|
|
@@ -219,28 +252,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
254
|
|
|
222
|
-
// ../render/src/host.ts
|
|
223
|
-
function intlFormatMoney(currencyCode) {
|
|
224
|
-
return (cents) => {
|
|
225
|
-
try {
|
|
226
|
-
return new Intl.NumberFormat(void 0, {
|
|
227
|
-
style: "currency",
|
|
228
|
-
currency: currencyCode
|
|
229
|
-
}).format(cents / 100);
|
|
230
|
-
} catch {
|
|
231
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
236
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
237
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
238
|
-
return [
|
|
239
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
240
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
241
|
-
];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
255
|
// ../render/src/product/option-selects.ts
|
|
245
256
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
246
257
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -1177,12 +1188,11 @@ function bindDropdowns(root) {
|
|
|
1177
1188
|
}
|
|
1178
1189
|
|
|
1179
1190
|
// src/renderers/fixed.ts
|
|
1180
|
-
function renderFixedBundle(container, bundle,
|
|
1191
|
+
function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
1181
1192
|
const wc = bundle.widgetConfig;
|
|
1182
1193
|
const products = adaptProducts(bundle);
|
|
1183
1194
|
if (!products.length) return;
|
|
1184
1195
|
const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));
|
|
1185
|
-
if (outOfStockBehavior === "hide" && oosProducts.length > 0) return;
|
|
1186
1196
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1187
1197
|
const formatMoney = intlFormatMoney(currency);
|
|
1188
1198
|
const t = DEFAULT_STRINGS;
|
|
@@ -1207,7 +1217,9 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
|
|
|
1207
1217
|
});
|
|
1208
1218
|
shell.products.appendChild(row);
|
|
1209
1219
|
if (isOos) continue;
|
|
1210
|
-
const
|
|
1220
|
+
const sellable = resolveSellableVariant(product);
|
|
1221
|
+
if (sellable) product.selectedVariantId = sellable.id;
|
|
1222
|
+
const selected = sellable ?? findVariant(product, product.selectedVariantId);
|
|
1211
1223
|
hydrateRowControls(row, product, selected);
|
|
1212
1224
|
applyVariant(row, selected, product, formatMoney);
|
|
1213
1225
|
bindVariantSelects(
|
|
@@ -1802,6 +1814,17 @@ function unlock() {
|
|
|
1802
1814
|
window.scrollTo(0, savedY);
|
|
1803
1815
|
}
|
|
1804
1816
|
|
|
1817
|
+
// ../render/src/picker/sort.ts
|
|
1818
|
+
function sinkAddedRows(list, rowEls, isAdded) {
|
|
1819
|
+
const front = [];
|
|
1820
|
+
const back = [];
|
|
1821
|
+
for (let i = 0; i < rowEls.length; i++) {
|
|
1822
|
+
(isAdded(i) ? back : front).push(rowEls[i]);
|
|
1823
|
+
}
|
|
1824
|
+
for (const el2 of front) list.appendChild(el2);
|
|
1825
|
+
for (const el2 of back) list.appendChild(el2);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1805
1828
|
// ../render/src/mix-match/modal.ts
|
|
1806
1829
|
var ALL_TYPES = "__all__";
|
|
1807
1830
|
var CLOSE_TRANSITION_MS = 300;
|
|
@@ -2026,12 +2049,22 @@ function createPickerModal(deps) {
|
|
|
2026
2049
|
syncActiveFilterPill();
|
|
2027
2050
|
if (searchInput) searchInput.value = "";
|
|
2028
2051
|
filterProducts(searchInput ? searchInput.value : "");
|
|
2052
|
+
if (modalList) {
|
|
2053
|
+
const selectedProductIds = /* @__PURE__ */ Object.create(null);
|
|
2054
|
+
for (const it of items) selectedProductIds[String(it.productId)] = true;
|
|
2055
|
+
sinkAddedRows(
|
|
2056
|
+
modalList,
|
|
2057
|
+
rows.map((r) => r.el),
|
|
2058
|
+
(i) => selectedProductIds[String(eligibleProducts[i].id)] === true
|
|
2059
|
+
);
|
|
2060
|
+
modalList.scrollTop = 0;
|
|
2061
|
+
}
|
|
2029
2062
|
overlay.style.display = "";
|
|
2030
2063
|
void overlay.offsetHeight;
|
|
2031
2064
|
overlay.classList.add("lb-mix-match__modal-overlay--open");
|
|
2032
2065
|
lock();
|
|
2033
2066
|
setTimeout(() => {
|
|
2034
|
-
if (
|
|
2067
|
+
if (closeBtn) closeBtn.focus();
|
|
2035
2068
|
else modalDialog?.focus();
|
|
2036
2069
|
}, 50);
|
|
2037
2070
|
}
|
|
@@ -2588,7 +2621,7 @@ function buildPickerModal(opts) {
|
|
|
2588
2621
|
}
|
|
2589
2622
|
|
|
2590
2623
|
// src/renderers/mix-match.ts
|
|
2591
|
-
function renderMixMatchBundle(container, bundle,
|
|
2624
|
+
function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, currentProductHandle) {
|
|
2592
2625
|
const wc = bundle.widgetConfig;
|
|
2593
2626
|
const t = DEFAULT_STRINGS;
|
|
2594
2627
|
const requiredQty = bundle.minQuantity || 1;
|
|
@@ -2611,12 +2644,12 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2611
2644
|
compareAtPrice: v.compareAtPrice,
|
|
2612
2645
|
unitPrice: v.unitPrice,
|
|
2613
2646
|
image: v.image,
|
|
2614
|
-
inventoryQuantity: null
|
|
2647
|
+
inventoryQuantity: v.inventoryQuantity ?? null
|
|
2615
2648
|
}))
|
|
2616
2649
|
}));
|
|
2617
2650
|
if (!eligibleProducts.length) return;
|
|
2618
2651
|
const poolUnits = eligibleProducts.filter((p) => p.available).length;
|
|
2619
|
-
if (
|
|
2652
|
+
if (poolUnits === 0) return;
|
|
2620
2653
|
const formatMoney = intlFormatMoney(
|
|
2621
2654
|
bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
|
|
2622
2655
|
);
|
|
@@ -2779,6 +2812,44 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2779
2812
|
// src/renderers/volume.ts
|
|
2780
2813
|
var import_core4 = require("@lime-bundles/core");
|
|
2781
2814
|
|
|
2815
|
+
// ../render/src/volume/stock.ts
|
|
2816
|
+
var TIER_OOS_CLASS = "lb-volume__tier--oos";
|
|
2817
|
+
function tierQty(el2) {
|
|
2818
|
+
return parseInt(el2.getAttribute("data-tier-qty") ?? "", 10) || 0;
|
|
2819
|
+
}
|
|
2820
|
+
function tierUnavailable(el2) {
|
|
2821
|
+
return el2.getAttribute("aria-disabled") === "true";
|
|
2822
|
+
}
|
|
2823
|
+
function applyTierStockState(tierEls, cap) {
|
|
2824
|
+
for (let i = 0; i < tierEls.length; i++) {
|
|
2825
|
+
const el2 = tierEls[i];
|
|
2826
|
+
const unavailable = cap !== null && tierQty(el2) > cap;
|
|
2827
|
+
if (unavailable) {
|
|
2828
|
+
el2.classList.add(TIER_OOS_CLASS);
|
|
2829
|
+
el2.setAttribute("aria-disabled", "true");
|
|
2830
|
+
} else {
|
|
2831
|
+
el2.classList.remove(TIER_OOS_CLASS);
|
|
2832
|
+
el2.removeAttribute("aria-disabled");
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function resolveAvailableTierIndex(tierEls, preferred) {
|
|
2837
|
+
const p = tierEls[preferred];
|
|
2838
|
+
if (p && !tierUnavailable(p)) return preferred;
|
|
2839
|
+
for (let i = 0; i < tierEls.length; i++) {
|
|
2840
|
+
if (!tierUnavailable(tierEls[i])) return i;
|
|
2841
|
+
}
|
|
2842
|
+
return -1;
|
|
2843
|
+
}
|
|
2844
|
+
function stepToAvailableTier(tierEls, from, direction) {
|
|
2845
|
+
let i = from + direction;
|
|
2846
|
+
while (i >= 0 && i < tierEls.length) {
|
|
2847
|
+
if (!tierUnavailable(tierEls[i])) return i;
|
|
2848
|
+
i += direction;
|
|
2849
|
+
}
|
|
2850
|
+
return from;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2782
2853
|
// ../render/src/volume/pricing.ts
|
|
2783
2854
|
function calcTierPrice(basePrice, discountType, tierEl) {
|
|
2784
2855
|
if (discountType === "fixed_amount") {
|
|
@@ -2868,15 +2939,16 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2868
2939
|
}
|
|
2869
2940
|
|
|
2870
2941
|
// src/renderers/volume.ts
|
|
2871
|
-
function renderVolumeBundle(container, bundle,
|
|
2942
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2872
2943
|
const wc = bundle.widgetConfig;
|
|
2873
|
-
const product = bundle.products
|
|
2944
|
+
const product = (0, import_core4.resolveContextProduct)(bundle.products, productContext);
|
|
2874
2945
|
if (!product) return;
|
|
2875
2946
|
const variants = product.variants.nodes;
|
|
2876
|
-
const
|
|
2947
|
+
const minTierQty = Math.min(
|
|
2948
|
+
...bundle.volumeTiers.map((tier) => tier.minQuantity)
|
|
2949
|
+
);
|
|
2950
|
+
const pricingVariant = variants.find((v) => (0, import_core4.isVariantFulfillable)(v, minTierQty)) ?? variants.find((v) => v.availableForSale) ?? variants[0];
|
|
2877
2951
|
if (!pricingVariant) return;
|
|
2878
|
-
const anyAvailable = variants.some((v) => v.availableForSale);
|
|
2879
|
-
if (outOfStockBehavior === "hide" && !anyAvailable) return;
|
|
2880
2952
|
const basePrice = toCents(pricingVariant.price.amount);
|
|
2881
2953
|
const discountType = bundle.discountConfig.discountType;
|
|
2882
2954
|
const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
|
|
@@ -2927,28 +2999,31 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
|
|
|
2927
2999
|
shell.products.replaceWith(group);
|
|
2928
3000
|
const tierEls = group.querySelectorAll("[data-tier-index]");
|
|
2929
3001
|
updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
|
|
2930
|
-
|
|
3002
|
+
applyTierStockState(tierEls, (0, import_core4.stockCapForVariant)(pricingVariant));
|
|
3003
|
+
let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);
|
|
3004
|
+
const noTierAvailable = selectedIndex === -1;
|
|
3005
|
+
if (noTierAvailable) selectedIndex = defaultIdx;
|
|
2931
3006
|
const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
|
|
2932
3007
|
group.addEventListener("click", (e) => {
|
|
2933
3008
|
const el2 = e.target.closest("[data-tier-index]");
|
|
2934
|
-
if (!el2) return;
|
|
3009
|
+
if (!el2 || tierUnavailable(el2)) return;
|
|
2935
3010
|
selectedIndex = parseInt(el2.getAttribute("data-tier-index") ?? "", 10) || 0;
|
|
2936
3011
|
apply();
|
|
2937
3012
|
});
|
|
2938
3013
|
group.addEventListener("keydown", (e) => {
|
|
2939
3014
|
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
|
2940
3015
|
e.preventDefault();
|
|
2941
|
-
selectedIndex =
|
|
3016
|
+
selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);
|
|
2942
3017
|
apply();
|
|
2943
3018
|
tierEls[selectedIndex].focus();
|
|
2944
3019
|
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
2945
3020
|
e.preventDefault();
|
|
2946
|
-
selectedIndex =
|
|
3021
|
+
selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);
|
|
2947
3022
|
apply();
|
|
2948
3023
|
tierEls[selectedIndex].focus();
|
|
2949
3024
|
}
|
|
2950
3025
|
});
|
|
2951
|
-
if (
|
|
3026
|
+
if (noTierAvailable) {
|
|
2952
3027
|
shell.cta.disabled = true;
|
|
2953
3028
|
const label = shell.cta.querySelector("[data-cta-label]");
|
|
2954
3029
|
if (label) label.textContent = t.outOfStock || "Out of stock";
|
|
@@ -3083,7 +3158,7 @@ function buildBogoPlus() {
|
|
|
3083
3158
|
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>';
|
|
3084
3159
|
return plus;
|
|
3085
3160
|
}
|
|
3086
|
-
function renderBogoBundle(container, bundle,
|
|
3161
|
+
function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3087
3162
|
const wc = bundle.widgetConfig;
|
|
3088
3163
|
const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
|
|
3089
3164
|
const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);
|
|
@@ -3112,7 +3187,6 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
|
|
|
3112
3187
|
}
|
|
3113
3188
|
];
|
|
3114
3189
|
const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;
|
|
3115
|
-
if (outOfStockBehavior === "hide" && oosCount > 0) return;
|
|
3116
3190
|
const formatMoney = intlFormatMoney(
|
|
3117
3191
|
buyProduct.priceRange.minVariantPrice.currencyCode
|
|
3118
3192
|
);
|
|
@@ -3545,6 +3619,14 @@ function createWizard(deps) {
|
|
|
3545
3619
|
rows.push(row);
|
|
3546
3620
|
modalList.appendChild(row.el);
|
|
3547
3621
|
}
|
|
3622
|
+
const stepPicks = selections[forStep] || [];
|
|
3623
|
+
const selectedProductIds = /* @__PURE__ */ Object.create(null);
|
|
3624
|
+
for (const it of stepPicks) selectedProductIds[String(it.productId)] = true;
|
|
3625
|
+
sinkAddedRows(
|
|
3626
|
+
modalList,
|
|
3627
|
+
rows.map((r) => r.el),
|
|
3628
|
+
(i) => selectedProductIds[String(pool[i].id)] === true
|
|
3629
|
+
);
|
|
3548
3630
|
deps.dropdown?.bindAll(modalList);
|
|
3549
3631
|
}
|
|
3550
3632
|
function onListClick(e) {
|
|
@@ -3707,6 +3789,7 @@ function createWizard(deps) {
|
|
|
3707
3789
|
syncActiveFilterPill();
|
|
3708
3790
|
if (searchInput) searchInput.value = "";
|
|
3709
3791
|
filterProducts("");
|
|
3792
|
+
if (modalList) modalList.scrollTop = 0;
|
|
3710
3793
|
buildStepSegments(stepIndex);
|
|
3711
3794
|
refresh();
|
|
3712
3795
|
if (modalLive) {
|
|
@@ -3730,7 +3813,7 @@ function createWizard(deps) {
|
|
|
3730
3813
|
overlay.classList.add("lb-mix-match__modal-overlay--open");
|
|
3731
3814
|
lock();
|
|
3732
3815
|
setTimeout(() => {
|
|
3733
|
-
if (
|
|
3816
|
+
if (closeBtn) closeBtn.focus();
|
|
3734
3817
|
else modalDialog?.focus();
|
|
3735
3818
|
}, 50);
|
|
3736
3819
|
}
|
|
@@ -3798,7 +3881,7 @@ function createWizard(deps) {
|
|
|
3798
3881
|
}
|
|
3799
3882
|
|
|
3800
3883
|
// src/renderers/multi-step.ts
|
|
3801
|
-
function renderMultiStepBundle(container, bundle,
|
|
3884
|
+
function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3802
3885
|
const wc = bundle.widgetConfig;
|
|
3803
3886
|
const t = DEFAULT_STRINGS;
|
|
3804
3887
|
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
@@ -3831,13 +3914,13 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
|
|
|
3831
3914
|
compareAtPrice: v.compareAtPrice,
|
|
3832
3915
|
unitPrice: v.unitPrice,
|
|
3833
3916
|
image: v.image,
|
|
3834
|
-
inventoryQuantity: null
|
|
3917
|
+
inventoryQuantity: v.inventoryQuantity ?? null
|
|
3835
3918
|
}))
|
|
3836
3919
|
};
|
|
3837
3920
|
})
|
|
3838
3921
|
);
|
|
3839
|
-
const
|
|
3840
|
-
if (
|
|
3922
|
+
const everyStepPickable = pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));
|
|
3923
|
+
if (!everyStepPickable) return;
|
|
3841
3924
|
const formatMoney = intlFormatMoney(
|
|
3842
3925
|
bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
|
|
3843
3926
|
);
|
|
@@ -5649,7 +5732,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5649
5732
|
transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
|
|
5650
5733
|
}
|
|
5651
5734
|
|
|
5652
|
-
.lb-mix-match__modal-add:hover {
|
|
5735
|
+
.lb-mix-match__modal-add:hover:not(:disabled) {
|
|
5653
5736
|
opacity: 0.9;
|
|
5654
5737
|
}
|
|
5655
5738
|
|
|
@@ -5764,8 +5847,15 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5764
5847
|
color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);
|
|
5765
5848
|
}
|
|
5766
5849
|
|
|
5767
|
-
/* "Done" reuses the picker Add-button styling so it matches the Add buttons.
|
|
5850
|
+
/* "Done" reuses the picker Add-button styling so it matches the Add buttons.
|
|
5851
|
+
The flex centring + min-height pin the footer-button silhouette: the wizard
|
|
5852
|
+
Back (bundle-multi-step.css) copies these metrics so the pair stays
|
|
5853
|
+
equal-height whatever border widths the merchant's picker tokens set. */
|
|
5768
5854
|
.lb-mix-match__modal-done {
|
|
5855
|
+
display: inline-flex;
|
|
5856
|
+
align-items: center;
|
|
5857
|
+
justify-content: center;
|
|
5858
|
+
min-height: 40px;
|
|
5769
5859
|
padding: 10px 24px;
|
|
5770
5860
|
transition: opacity 0.25s ease;
|
|
5771
5861
|
background: var(--lb-picker-add-bg);
|
|
@@ -5779,7 +5869,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5779
5869
|
cursor: pointer;
|
|
5780
5870
|
}
|
|
5781
5871
|
|
|
5782
|
-
.lb-mix-match__modal-done:hover {
|
|
5872
|
+
.lb-mix-match__modal-done:hover:not(:disabled) {
|
|
5783
5873
|
opacity: 0.9;
|
|
5784
5874
|
}
|
|
5785
5875
|
|
|
@@ -5789,6 +5879,16 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
5789
5879
|
box-shadow: none;
|
|
5790
5880
|
}
|
|
5791
5881
|
|
|
5882
|
+
/* Disabled Next/Done (wizard step minimum not met) \u2014 same washed-out
|
|
5883
|
+
treatment as a disabled row Add, so an unfinished step reads at a
|
|
5884
|
+
glance. Hover is gated off above; the solid button returns the moment
|
|
5885
|
+
the requirement is met. */
|
|
5886
|
+
.lb-mix-match__modal-done:disabled {
|
|
5887
|
+
background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));
|
|
5888
|
+
color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);
|
|
5889
|
+
cursor: not-allowed;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5792
5892
|
/* Hidden utility for search filtering */
|
|
5793
5893
|
.lb-hidden {
|
|
5794
5894
|
display: none !important;
|
|
@@ -6045,6 +6145,13 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
|
|
|
6045
6145
|
white-space: nowrap;
|
|
6046
6146
|
font-variant-numeric: tabular-nums;
|
|
6047
6147
|
}
|
|
6148
|
+
|
|
6149
|
+
/* Tier the selected variant can't cover \u2014 same greyed treatment as an
|
|
6150
|
+
out-of-stock product row, and not selectable. */
|
|
6151
|
+
.lb-volume__tier--oos {
|
|
6152
|
+
opacity: 0.5;
|
|
6153
|
+
cursor: not-allowed;
|
|
6154
|
+
}
|
|
6048
6155
|
`;
|
|
6049
6156
|
var BUNDLE_BOGO_CSS = `/* Lime Bundles \u2014 BOGO (Buy X Get Y) bundle styles */
|
|
6050
6157
|
|
|
@@ -6213,14 +6320,23 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6213
6320
|
justify-self: end;
|
|
6214
6321
|
}
|
|
6215
6322
|
|
|
6216
|
-
/* Back \u2014 secondary treatment beside the primary Next/Done
|
|
6217
|
-
.lb-mix-match__modal-done).
|
|
6323
|
+
/* Back \u2014 secondary (outlined) treatment beside the primary Next/Done
|
|
6324
|
+
(which reuse .lb-mix-match__modal-done). Same silhouette as Next \u2014
|
|
6325
|
+
metrics copied from the modal-done recipe (padding, type, radius,
|
|
6326
|
+
min-height) \u2014 and picker tokens only: the modal is portaled out of the
|
|
6327
|
+
widget, so the widget vars (--lb-text, --lb-border-*) it previously
|
|
6328
|
+
used don't reliably resolve here and break palette isolation. */
|
|
6218
6329
|
.lb-multi-step__modal-back {
|
|
6219
|
-
|
|
6330
|
+
display: inline-flex;
|
|
6331
|
+
align-items: center;
|
|
6332
|
+
justify-content: center;
|
|
6333
|
+
min-height: 40px;
|
|
6334
|
+
padding: 10px 24px;
|
|
6220
6335
|
background: none;
|
|
6221
|
-
border: var(--lb-border-width) solid var(--lb-border-color);
|
|
6222
|
-
border-radius: var(--lb-radius-sm);
|
|
6223
|
-
|
|
6336
|
+
border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
|
|
6337
|
+
border-radius: var(--lb-picker-radius-sm);
|
|
6338
|
+
box-sizing: border-box;
|
|
6339
|
+
color: var(--lb-picker-text);
|
|
6224
6340
|
font-family: inherit;
|
|
6225
6341
|
font-size: 14px;
|
|
6226
6342
|
font-weight: 600;
|
|
@@ -6229,7 +6345,7 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6229
6345
|
}
|
|
6230
6346
|
|
|
6231
6347
|
.lb-multi-step__modal-back:hover {
|
|
6232
|
-
background: color-mix(in srgb, var(--lb-text) 5%, transparent);
|
|
6348
|
+
background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);
|
|
6233
6349
|
}
|
|
6234
6350
|
|
|
6235
6351
|
.lb-multi-step__modal-back:focus-visible {
|
|
@@ -6621,6 +6737,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6621
6737
|
"storefront-token",
|
|
6622
6738
|
"bundle-gid",
|
|
6623
6739
|
"product-handle",
|
|
6740
|
+
"product-id",
|
|
6624
6741
|
"app-url",
|
|
6625
6742
|
"analytics",
|
|
6626
6743
|
"locale",
|
|
@@ -6659,12 +6776,6 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6659
6776
|
* `.lb-bundle-widget { ... }` reach the widget's DOM.
|
|
6660
6777
|
*/
|
|
6661
6778
|
shopCustomCss = null;
|
|
6662
|
-
/**
|
|
6663
|
-
* Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
|
|
6664
|
-
* before the first render. Defaults to `show_greyed_out` so a failed or
|
|
6665
|
-
* missing fetch degrades to "bundles still render".
|
|
6666
|
-
*/
|
|
6667
|
-
outOfStockBehavior = import_core6.DEFAULT_OUT_OF_STOCK_BEHAVIOR;
|
|
6668
6779
|
constructor() {
|
|
6669
6780
|
super();
|
|
6670
6781
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
@@ -6687,7 +6798,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6687
6798
|
}
|
|
6688
6799
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6689
6800
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6690
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
6801
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6691
6802
|
if (this.shopDomain && this.storefrontToken) {
|
|
6692
6803
|
this.fetchBundle();
|
|
6693
6804
|
}
|
|
@@ -6705,6 +6816,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6705
6816
|
get productHandleAttr() {
|
|
6706
6817
|
return this.getAttribute("product-handle") ?? "";
|
|
6707
6818
|
}
|
|
6819
|
+
/**
|
|
6820
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
6821
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
6822
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
6823
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
6824
|
+
* the bundle's first product.
|
|
6825
|
+
*/
|
|
6826
|
+
get productIdAttr() {
|
|
6827
|
+
return this.getAttribute("product-id") ?? "";
|
|
6828
|
+
}
|
|
6708
6829
|
get appUrl() {
|
|
6709
6830
|
return this.getAttribute("app-url") ?? "";
|
|
6710
6831
|
}
|
|
@@ -6800,9 +6921,6 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6800
6921
|
const sanitized = (0, import_core6.sanitizeCustomCss)(customCss);
|
|
6801
6922
|
if (sanitized.ok) this.shopCustomCss = sanitized.css;
|
|
6802
6923
|
}
|
|
6803
|
-
this.outOfStockBehavior = (0, import_core6.parseOutOfStockBehavior)(
|
|
6804
|
-
shopSettings?.shop?.outOfStockBehavior?.value
|
|
6805
|
-
);
|
|
6806
6924
|
this.renderBundles();
|
|
6807
6925
|
} catch (err) {
|
|
6808
6926
|
if (controller.signal.aborted) return;
|
|
@@ -6881,17 +6999,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6881
6999
|
});
|
|
6882
7000
|
const storage = window.localStorage;
|
|
6883
7001
|
const key = cartStorageKey(this.shopDomain);
|
|
6884
|
-
|
|
7002
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
7003
|
+
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
7004
|
+
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
7005
|
+
const fail = (message) => {
|
|
7006
|
+
this.dispatchEvent(
|
|
7007
|
+
new CustomEvent("lime-bundle:error", {
|
|
7008
|
+
detail: { message, code: "CART_ERROR" },
|
|
7009
|
+
bubbles: true,
|
|
7010
|
+
composed: true
|
|
7011
|
+
})
|
|
7012
|
+
);
|
|
7013
|
+
};
|
|
6885
7014
|
try {
|
|
6886
7015
|
let checkoutUrl = null;
|
|
6887
|
-
if (
|
|
7016
|
+
if (cartId) {
|
|
7017
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7018
|
+
client,
|
|
7019
|
+
cartId,
|
|
7020
|
+
() => storage?.removeItem(key)
|
|
7021
|
+
);
|
|
7022
|
+
}
|
|
7023
|
+
if (cartId) {
|
|
6888
7024
|
const res = await client.query(
|
|
6889
7025
|
import_core6.CART_LINES_ADD_MUTATION,
|
|
6890
|
-
{ cartId
|
|
7026
|
+
{ cartId, lines }
|
|
6891
7027
|
);
|
|
6892
7028
|
const payload = res.cartLinesAdd;
|
|
6893
7029
|
if (payload?.userErrors?.length) {
|
|
7030
|
+
if (!isStaleCartError(payload.userErrors)) {
|
|
7031
|
+
fail(payload.userErrors[0].message);
|
|
7032
|
+
return;
|
|
7033
|
+
}
|
|
6894
7034
|
storage?.removeItem(key);
|
|
7035
|
+
} else if (stockCapped(payload?.warnings)) {
|
|
7036
|
+
fail("Some items in this bundle are out of stock.");
|
|
7037
|
+
return;
|
|
6895
7038
|
} else if (payload?.cart) {
|
|
6896
7039
|
checkoutUrl = payload.cart.checkoutUrl;
|
|
6897
7040
|
}
|
|
@@ -6902,6 +7045,14 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6902
7045
|
{ input: { lines } }
|
|
6903
7046
|
);
|
|
6904
7047
|
const payload = res.cartCreate;
|
|
7048
|
+
if (payload?.userErrors?.length) {
|
|
7049
|
+
fail(payload.userErrors[0].message);
|
|
7050
|
+
return;
|
|
7051
|
+
}
|
|
7052
|
+
if (stockCapped(payload?.warnings)) {
|
|
7053
|
+
fail("Some items in this bundle are out of stock.");
|
|
7054
|
+
return;
|
|
7055
|
+
}
|
|
6905
7056
|
if (payload?.cart) {
|
|
6906
7057
|
storage?.setItem(key, payload.cart.id);
|
|
6907
7058
|
checkoutUrl = payload.cart.checkoutUrl;
|
|
@@ -6910,13 +7061,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6910
7061
|
if (checkoutUrl) {
|
|
6911
7062
|
window.location.assign(checkoutUrl);
|
|
6912
7063
|
} else {
|
|
6913
|
-
|
|
6914
|
-
new CustomEvent("lime-bundle:error", {
|
|
6915
|
-
detail: { message: "Cart creation failed", code: "CART_ERROR" },
|
|
6916
|
-
bubbles: true,
|
|
6917
|
-
composed: true
|
|
6918
|
-
})
|
|
6919
|
-
);
|
|
7064
|
+
fail("Cart creation failed");
|
|
6920
7065
|
}
|
|
6921
7066
|
} catch (err) {
|
|
6922
7067
|
this.dispatchEvent(
|
|
@@ -6931,6 +7076,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6931
7076
|
);
|
|
6932
7077
|
}
|
|
6933
7078
|
}
|
|
7079
|
+
/**
|
|
7080
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7081
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7082
|
+
* clicked add three times while deciding checks out with only what they
|
|
7083
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7084
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7085
|
+
* left alone.
|
|
7086
|
+
*
|
|
7087
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7088
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7089
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7090
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7091
|
+
* checkout matches the shopper's current build.
|
|
7092
|
+
*/
|
|
7093
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7094
|
+
const res = await client.query(import_core6.CART_LINES_QUERY, {
|
|
7095
|
+
cartId
|
|
7096
|
+
});
|
|
7097
|
+
if (!res.cart) {
|
|
7098
|
+
dropSavedCart();
|
|
7099
|
+
return null;
|
|
7100
|
+
}
|
|
7101
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7102
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7103
|
+
).map((line) => line.id);
|
|
7104
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7105
|
+
const removal = await client.query(
|
|
7106
|
+
import_core6.CART_LINES_REMOVE_MUTATION,
|
|
7107
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7108
|
+
);
|
|
7109
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7110
|
+
dropSavedCart();
|
|
7111
|
+
return null;
|
|
7112
|
+
}
|
|
7113
|
+
return cartId;
|
|
7114
|
+
}
|
|
6934
7115
|
reportAddToCartEvent(bundle, lines) {
|
|
6935
7116
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
6936
7117
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -6990,44 +7171,28 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6990
7171
|
const registerCleanup = (fn) => this.renderCleanups.push(fn);
|
|
6991
7172
|
switch (bundle.bundleType) {
|
|
6992
7173
|
case "fixed":
|
|
6993
|
-
renderFixedBundle(
|
|
6994
|
-
container,
|
|
6995
|
-
bundle,
|
|
6996
|
-
this.outOfStockBehavior,
|
|
6997
|
-
dispatch,
|
|
6998
|
-
registerCleanup
|
|
6999
|
-
);
|
|
7174
|
+
renderFixedBundle(container, bundle, dispatch, registerCleanup);
|
|
7000
7175
|
break;
|
|
7001
7176
|
case "mix_match":
|
|
7002
7177
|
renderMixMatchBundle(
|
|
7003
7178
|
container,
|
|
7004
7179
|
bundle,
|
|
7005
|
-
this.outOfStockBehavior,
|
|
7006
7180
|
dispatch,
|
|
7007
7181
|
registerCleanup,
|
|
7008
7182
|
this.currentProductHandle
|
|
7009
7183
|
);
|
|
7010
7184
|
break;
|
|
7011
7185
|
case "volume":
|
|
7012
|
-
renderVolumeBundle(container, bundle,
|
|
7186
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7187
|
+
productId: this.productIdAttr || null,
|
|
7188
|
+
productHandle: this.currentProductHandle
|
|
7189
|
+
});
|
|
7013
7190
|
break;
|
|
7014
7191
|
case "bogo":
|
|
7015
|
-
renderBogoBundle(
|
|
7016
|
-
container,
|
|
7017
|
-
bundle,
|
|
7018
|
-
this.outOfStockBehavior,
|
|
7019
|
-
dispatch,
|
|
7020
|
-
registerCleanup
|
|
7021
|
-
);
|
|
7192
|
+
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|
|
7022
7193
|
break;
|
|
7023
7194
|
case "multi_step":
|
|
7024
|
-
renderMultiStepBundle(
|
|
7025
|
-
container,
|
|
7026
|
-
bundle,
|
|
7027
|
-
this.outOfStockBehavior,
|
|
7028
|
-
dispatch,
|
|
7029
|
-
registerCleanup
|
|
7030
|
-
);
|
|
7195
|
+
renderMultiStepBundle(container, bundle, dispatch, registerCleanup);
|
|
7031
7196
|
break;
|
|
7032
7197
|
}
|
|
7033
7198
|
this.shadow.appendChild(container);
|