@lime-bundles/widget 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +235 -86
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +217 -62
- package/dist/lime-bundle.global.js +34 -20
- package/dist/lime-bundle.global.js.map +1 -1
- package/docs/css-variables.md +7 -1
- package/docs/hydrogen.md +29 -4
- package/docs/react-nextjs.md +24 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
withInContext,
|
|
17
17
|
isVisibleInMarket,
|
|
18
18
|
parseOutOfStockBehavior,
|
|
19
|
-
DEFAULT_OUT_OF_STOCK_BEHAVIOR
|
|
19
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR,
|
|
20
|
+
getVisitorId,
|
|
21
|
+
resolveAbTests
|
|
20
22
|
} from "@lime-bundles/core";
|
|
21
23
|
|
|
22
24
|
// ../render/src/adapt.ts
|
|
@@ -506,7 +508,7 @@ function buildWidgetShell(typeClass, opts) {
|
|
|
506
508
|
"data-countdown": ""
|
|
507
509
|
});
|
|
508
510
|
const label2 = box("span", "lb-bundle-countdown-label");
|
|
509
|
-
label2.textContent = opts.countdownLabel ?? "
|
|
511
|
+
label2.textContent = opts.countdownLabel ?? "Limited time offer";
|
|
510
512
|
countdown.appendChild(label2);
|
|
511
513
|
countdown.appendChild(
|
|
512
514
|
box("span", "lb-bundle-countdown-timer", {
|
|
@@ -518,10 +520,12 @@ function buildWidgetShell(typeClass, opts) {
|
|
|
518
520
|
const products = box("div", typeClass + "__products lb-edge-fade");
|
|
519
521
|
root.appendChild(products);
|
|
520
522
|
root.appendChild(box("div", "lb-bundle-divider"));
|
|
521
|
-
const summary = box("div", "lb-bundle-summary"
|
|
523
|
+
const summary = box("div", "lb-bundle-summary", {
|
|
524
|
+
"data-pricing-section": ""
|
|
525
|
+
});
|
|
522
526
|
const text = box("div", "lb-bundle-summary__text");
|
|
523
527
|
const label = box("span", "lb-bundle-summary__label");
|
|
524
|
-
label.textContent = opts.summaryLabel ?? "Bundle
|
|
528
|
+
label.textContent = opts.summaryLabel ?? "Bundle total";
|
|
525
529
|
text.appendChild(label);
|
|
526
530
|
if (opts.withItemCount) {
|
|
527
531
|
label.appendChild(box("span", "", { "data-item-count": "" }));
|
|
@@ -552,6 +556,7 @@ function buildWidgetShell(typeClass, opts) {
|
|
|
552
556
|
summary.appendChild(prices);
|
|
553
557
|
root.appendChild(summary);
|
|
554
558
|
const cta = box("button", "lb-bundle-cta", {
|
|
559
|
+
"data-cta-text": opts.ctaText,
|
|
555
560
|
type: "button",
|
|
556
561
|
"data-add-bundle": ""
|
|
557
562
|
});
|
|
@@ -570,7 +575,9 @@ function buildWidgetShell(typeClass, opts) {
|
|
|
570
575
|
);
|
|
571
576
|
return { root, products, cta };
|
|
572
577
|
}
|
|
573
|
-
function buildVolumeTiers(tiers, groupLabel) {
|
|
578
|
+
function buildVolumeTiers(tiers, groupLabel, opts = {}) {
|
|
579
|
+
const showCompare = opts.showCompare !== false;
|
|
580
|
+
const showPriceEach = opts.showPriceEach !== false;
|
|
574
581
|
const group = box("div", "lb-volume__tiers lb-edge-fade", {
|
|
575
582
|
role: "radiogroup",
|
|
576
583
|
"aria-label": groupLabel,
|
|
@@ -595,8 +602,17 @@ function buildVolumeTiers(tiers, groupLabel) {
|
|
|
595
602
|
label.textContent = tier.label;
|
|
596
603
|
info.appendChild(label);
|
|
597
604
|
const price = box("span", "lb-volume__tier-price");
|
|
598
|
-
|
|
599
|
-
|
|
605
|
+
if (showCompare && (tier.percent > 0 || tier.amountCents > 0)) {
|
|
606
|
+
price.appendChild(box("span", "lb-volume__tier-compare"));
|
|
607
|
+
}
|
|
608
|
+
if (showPriceEach) {
|
|
609
|
+
price.appendChild(box("span", "", { "data-tier-price-each": "" }));
|
|
610
|
+
if (opts.unitLabel) {
|
|
611
|
+
const unit = box("span", "lb-volume__tier-unit");
|
|
612
|
+
unit.textContent = opts.unitLabel;
|
|
613
|
+
price.appendChild(unit);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
600
616
|
info.appendChild(price);
|
|
601
617
|
el2.appendChild(info);
|
|
602
618
|
if (tier.badgeLabel || tier.savingsLabel) {
|
|
@@ -705,7 +721,10 @@ var DEFAULT_STRINGS = {
|
|
|
705
721
|
required: "Required",
|
|
706
722
|
soldOut: "Sold out",
|
|
707
723
|
outOfStock: "Out of stock",
|
|
708
|
-
itemsOutOfStock:
|
|
724
|
+
itemsOutOfStock: {
|
|
725
|
+
one: "__COUNT__ item out of stock",
|
|
726
|
+
other: "__COUNT__ items out of stock"
|
|
727
|
+
},
|
|
709
728
|
quantity: "Quantity",
|
|
710
729
|
increaseQuantity: "Increase quantity",
|
|
711
730
|
decreaseQuantity: "Decrease quantity",
|
|
@@ -725,11 +744,26 @@ var DEFAULT_STRINGS = {
|
|
|
725
744
|
stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
|
|
726
745
|
each: " each",
|
|
727
746
|
buyQty: "Buy __COUNT__",
|
|
747
|
+
itemCount: {
|
|
748
|
+
one: "__COUNT__ item",
|
|
749
|
+
other: "__COUNT__ items"
|
|
750
|
+
},
|
|
728
751
|
needsSpots: {
|
|
729
752
|
one: "Needs __COUNT__ spot",
|
|
730
753
|
other: "Needs __COUNT__ spots"
|
|
731
754
|
}
|
|
732
755
|
};
|
|
756
|
+
function formatItemsOutOfStock(t, count) {
|
|
757
|
+
const raw = t.itemsOutOfStock;
|
|
758
|
+
let tpl;
|
|
759
|
+
if (raw && typeof raw === "object") {
|
|
760
|
+
const form = new Intl.PluralRules(t.locale || "en").select(count);
|
|
761
|
+
tpl = raw[form] || raw.other;
|
|
762
|
+
} else {
|
|
763
|
+
tpl = raw;
|
|
764
|
+
}
|
|
765
|
+
return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
|
|
766
|
+
}
|
|
733
767
|
|
|
734
768
|
// ../render/src/dropdown/bind.ts
|
|
735
769
|
import { dropdown } from "@lime-bundles/core";
|
|
@@ -1186,7 +1220,7 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
|
|
|
1186
1220
|
shell.cta.disabled = true;
|
|
1187
1221
|
const label = shell.cta.querySelector("[data-cta-label]");
|
|
1188
1222
|
if (label) {
|
|
1189
|
-
label.textContent = (t
|
|
1223
|
+
label.textContent = formatItemsOutOfStock(t, oosProducts.length);
|
|
1190
1224
|
}
|
|
1191
1225
|
} else {
|
|
1192
1226
|
shell.cta.addEventListener("click", () => {
|
|
@@ -1244,15 +1278,38 @@ function formatSubtitle(remaining, discountType, discountValue, t) {
|
|
|
1244
1278
|
|
|
1245
1279
|
// ../render/src/picker/rules.ts
|
|
1246
1280
|
var DEFAULT_RULE = { min: 1, max: 99 };
|
|
1247
|
-
function ruleFor(productId, rulesMap) {
|
|
1281
|
+
function ruleFor(productId, rulesMap, variantId) {
|
|
1282
|
+
if (variantId != null) {
|
|
1283
|
+
const vRule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1284
|
+
if (vRule && typeof vRule.min === "number" && typeof vRule.max === "number") {
|
|
1285
|
+
return vRule;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1248
1288
|
const rule = rulesMap["gid://shopify/Product/" + productId];
|
|
1249
1289
|
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1250
1290
|
return DEFAULT_RULE;
|
|
1251
1291
|
}
|
|
1252
1292
|
return rule;
|
|
1253
1293
|
}
|
|
1254
|
-
function
|
|
1255
|
-
|
|
1294
|
+
function variantRuleFor(variantId, rulesMap) {
|
|
1295
|
+
if (variantId == null) return void 0;
|
|
1296
|
+
const rule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1297
|
+
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1298
|
+
return void 0;
|
|
1299
|
+
}
|
|
1300
|
+
return rule;
|
|
1301
|
+
}
|
|
1302
|
+
function mergeRuleMaps(productRules, variantRules) {
|
|
1303
|
+
const products = productRules ?? {};
|
|
1304
|
+
if (!variantRules) return products;
|
|
1305
|
+
const keys = Object.keys(variantRules);
|
|
1306
|
+
if (keys.length === 0) return products;
|
|
1307
|
+
const merged = { ...products };
|
|
1308
|
+
for (const key of keys) merged[key] = variantRules[key];
|
|
1309
|
+
return merged;
|
|
1310
|
+
}
|
|
1311
|
+
function defaultPickQuantity(productId, rulesMap, variantId) {
|
|
1312
|
+
return ruleFor(productId, rulesMap, variantId).min;
|
|
1256
1313
|
}
|
|
1257
1314
|
function totalUnits(items) {
|
|
1258
1315
|
let qty = 0;
|
|
@@ -1272,11 +1329,9 @@ function committedQtyForVariant(items, variantId) {
|
|
|
1272
1329
|
function isRowAddable(rule, remainingSpots) {
|
|
1273
1330
|
return rule.min <= remainingSpots;
|
|
1274
1331
|
}
|
|
1275
|
-
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
ownQty + remainingSpots
|
|
1279
|
-
);
|
|
1332
|
+
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0, variantMax) {
|
|
1333
|
+
let ceiling = Math.min(rule.max - productOtherUnits, ownQty + remainingSpots);
|
|
1334
|
+
if (typeof variantMax === "number") ceiling = Math.min(ceiling, variantMax);
|
|
1280
1335
|
return typeof stock === "number" ? Math.min(ceiling, stock) : ceiling;
|
|
1281
1336
|
}
|
|
1282
1337
|
|
|
@@ -1355,7 +1410,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1355
1410
|
}
|
|
1356
1411
|
infoDiv.appendChild(titleEl);
|
|
1357
1412
|
let currentVariant = firstAvailable ?? (p.variants.length > 0 ? p.variants[0] : null);
|
|
1358
|
-
const
|
|
1413
|
+
const ruleForVariant = (v) => ruleFor(p.id, rulesMap, v ? v.id : null);
|
|
1414
|
+
const modalQty = currentVariant ? ruleForVariant(currentVariant).min : 1;
|
|
1359
1415
|
const priceEl = document.createElement("p");
|
|
1360
1416
|
priceEl.className = "lb-mix-match__modal-product-price";
|
|
1361
1417
|
priceEl.setAttribute("data-row-price", "");
|
|
@@ -1391,8 +1447,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1391
1447
|
infoDiv.appendChild(unitPriceEl);
|
|
1392
1448
|
let qtyGroup = null;
|
|
1393
1449
|
let qtyValueEl = null;
|
|
1394
|
-
let current =
|
|
1395
|
-
let effectiveMax =
|
|
1450
|
+
let current = ruleForVariant(currentVariant).min;
|
|
1451
|
+
let effectiveMax = ruleForVariant(currentVariant).max;
|
|
1396
1452
|
let paint = null;
|
|
1397
1453
|
let setBoundsForVariant = null;
|
|
1398
1454
|
if (p.available && ctx.showQtySelector) {
|
|
@@ -1414,7 +1470,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1414
1470
|
qtyValueEl.className = "lb-mix-match__qty-stepper-value";
|
|
1415
1471
|
qtyValueEl.setAttribute("data-row-qty", "");
|
|
1416
1472
|
qtyValueEl.setAttribute("aria-live", "polite");
|
|
1417
|
-
qtyValueEl.textContent = String(
|
|
1473
|
+
qtyValueEl.textContent = String(ruleForVariant(currentVariant).min);
|
|
1418
1474
|
const qtyPlusBtn = document.createElement("button");
|
|
1419
1475
|
qtyPlusBtn.type = "button";
|
|
1420
1476
|
qtyPlusBtn.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus";
|
|
@@ -1431,7 +1487,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1431
1487
|
paint = () => {
|
|
1432
1488
|
valueEl.textContent = String(current);
|
|
1433
1489
|
valueEl.setAttribute("data-qty", String(current));
|
|
1434
|
-
qtyMinusBtn.disabled = stepperDisabled || current <=
|
|
1490
|
+
qtyMinusBtn.disabled = stepperDisabled || current <= ruleForVariant(currentVariant).min;
|
|
1435
1491
|
qtyPlusBtn.disabled = stepperDisabled || current >= effectiveMax;
|
|
1436
1492
|
};
|
|
1437
1493
|
setBoundsForVariant = (variant, alreadyInBundle = 0) => {
|
|
@@ -1442,18 +1498,21 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1442
1498
|
const stock = variant ? variant.inventoryQuantity : null;
|
|
1443
1499
|
const othersInBundle = Math.max(0, alreadyInBundle - ownQty);
|
|
1444
1500
|
const stockCap = typeof stock === "number" ? Math.max(0, stock - othersInBundle) : void 0;
|
|
1501
|
+
const vMax = variantRuleFor(variantId, rulesMap)?.max;
|
|
1502
|
+
const rMin = ruleForVariant(variant).min;
|
|
1445
1503
|
effectiveMax = swapUnits > 0 ? typeof stockCap === "number" ? Math.min(swapUnits, stockCap) : swapUnits : stepperCeiling(
|
|
1446
1504
|
pRule,
|
|
1447
1505
|
capacity.remainingSpots(),
|
|
1448
1506
|
stockCap,
|
|
1449
1507
|
ownQty,
|
|
1450
|
-
otherUnits
|
|
1508
|
+
otherUnits,
|
|
1509
|
+
vMax
|
|
1451
1510
|
);
|
|
1452
|
-
if (effectiveMax <
|
|
1453
|
-
current =
|
|
1511
|
+
if (effectiveMax < rMin) {
|
|
1512
|
+
current = rMin;
|
|
1454
1513
|
} else {
|
|
1455
1514
|
if (current > effectiveMax) current = effectiveMax;
|
|
1456
|
-
if (current <
|
|
1515
|
+
if (current < rMin) current = rMin;
|
|
1457
1516
|
}
|
|
1458
1517
|
paint();
|
|
1459
1518
|
};
|
|
@@ -1464,7 +1523,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1464
1523
|
}
|
|
1465
1524
|
};
|
|
1466
1525
|
qtyMinusBtn.addEventListener("click", () => {
|
|
1467
|
-
if (current >
|
|
1526
|
+
if (current > ruleForVariant(currentVariant).min) current -= 1;
|
|
1468
1527
|
paint();
|
|
1469
1528
|
commitQtyToBundle();
|
|
1470
1529
|
});
|
|
@@ -1534,7 +1593,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1534
1593
|
}
|
|
1535
1594
|
currentVariant = v;
|
|
1536
1595
|
row.setAttribute("data-selected-variant-id", String(v.id));
|
|
1537
|
-
const resolvedQty =
|
|
1596
|
+
const resolvedQty = ruleForVariant(v).min;
|
|
1538
1597
|
priceSaleEl.textContent = formatMoney(v.price * resolvedQty);
|
|
1539
1598
|
paintRowCompare(priceCompareEl, v, resolvedQty);
|
|
1540
1599
|
if (v.unitPrice) {
|
|
@@ -1636,17 +1695,19 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1636
1695
|
if (setBoundsForVariant) {
|
|
1637
1696
|
const committed = capacity.ownQty(variant.id);
|
|
1638
1697
|
if (committed > 0) current = committed;
|
|
1639
|
-
else if (lastVariantId !== variant.id)
|
|
1698
|
+
else if (lastVariantId !== variant.id)
|
|
1699
|
+
current = ruleForVariant(variant).min;
|
|
1640
1700
|
lastVariantId = variant.id;
|
|
1641
1701
|
setBoundsForVariant(variant, already);
|
|
1642
1702
|
}
|
|
1643
1703
|
const rowSwapUnits = capacity.swapUnits(p.id, variant.id);
|
|
1644
|
-
const
|
|
1704
|
+
const vRule = ruleForVariant(variant);
|
|
1705
|
+
const inSwapState = rowSwapUnits >= vRule.min;
|
|
1645
1706
|
const remaining = capacity.remainingSpots();
|
|
1646
|
-
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(
|
|
1707
|
+
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(vRule, remaining);
|
|
1647
1708
|
if (needsSpotsEl) {
|
|
1648
1709
|
if (needsMoreSpots && remaining > 0) {
|
|
1649
|
-
needsSpotsEl.textContent = formatNeedsSpots(
|
|
1710
|
+
needsSpotsEl.textContent = formatNeedsSpots(vRule.min, t);
|
|
1650
1711
|
needsSpotsEl.hidden = false;
|
|
1651
1712
|
} else {
|
|
1652
1713
|
needsSpotsEl.hidden = true;
|
|
@@ -1683,11 +1744,11 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1683
1744
|
addBtn.classList.remove("lb-mix-match__modal-add--required");
|
|
1684
1745
|
row.classList.remove("lb-mix-match__modal-product--in-bundle");
|
|
1685
1746
|
addedBadge.hidden = true;
|
|
1686
|
-
const noStock = typeof stock === "number" ? Math.max(0, stock - already) <
|
|
1747
|
+
const noStock = typeof stock === "number" ? Math.max(0, stock - already) < vRule.min : false;
|
|
1687
1748
|
if (inSwapState) {
|
|
1688
1749
|
addBtn.disabled = noStock;
|
|
1689
1750
|
} else {
|
|
1690
|
-
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) <
|
|
1751
|
+
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) < vRule.min;
|
|
1691
1752
|
addBtn.disabled = noStock || productMaxed || needsMoreSpots || capacity.isFull();
|
|
1692
1753
|
}
|
|
1693
1754
|
}
|
|
@@ -1839,7 +1900,7 @@ function createPickerModal(deps) {
|
|
|
1839
1900
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
1840
1901
|
10
|
|
1841
1902
|
) || 1
|
|
1842
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
1903
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
1843
1904
|
const newItem = {
|
|
1844
1905
|
productId: prod.id,
|
|
1845
1906
|
variantId: selectedVariant.id,
|
|
@@ -2048,6 +2109,7 @@ function productUnitsInOtherSlots(items, productId, excludeVariantId) {
|
|
|
2048
2109
|
return qty;
|
|
2049
2110
|
}
|
|
2050
2111
|
function canRemovePick(items, item, rulesMap) {
|
|
2112
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
2051
2113
|
const rule = ruleFor(item.productId, rulesMap);
|
|
2052
2114
|
if (!rule.required) return true;
|
|
2053
2115
|
let unitsElsewhere = 0;
|
|
@@ -2312,8 +2374,17 @@ function seedSelection(eligibleProducts, rulesMap, requiredQty, courtesy) {
|
|
|
2312
2374
|
});
|
|
2313
2375
|
const required = [];
|
|
2314
2376
|
for (const p of eligibleProducts) {
|
|
2377
|
+
if (!p.available || !p.variants) continue;
|
|
2378
|
+
let seededVariant = false;
|
|
2379
|
+
for (const v3 of p.variants) {
|
|
2380
|
+
const vRule = variantRuleFor(v3.id, rulesMap);
|
|
2381
|
+
if (!vRule?.required || !v3.available) continue;
|
|
2382
|
+
required.push(pick(p, v3, Math.max(1, vRule.min)));
|
|
2383
|
+
seededVariant = true;
|
|
2384
|
+
}
|
|
2385
|
+
if (seededVariant) continue;
|
|
2315
2386
|
const rule = ruleFor(p.id, rulesMap);
|
|
2316
|
-
if (!rule.required
|
|
2387
|
+
if (!rule.required) continue;
|
|
2317
2388
|
const v2 = firstAvailable(p);
|
|
2318
2389
|
if (!v2) continue;
|
|
2319
2390
|
required.push(pick(p, v2, Math.max(1, rule.min)));
|
|
@@ -2514,17 +2585,17 @@ function buildPickerModal(opts) {
|
|
|
2514
2585
|
}
|
|
2515
2586
|
|
|
2516
2587
|
// src/renderers/mix-match.ts
|
|
2517
|
-
function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
|
|
2588
|
+
function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup, currentProductHandle) {
|
|
2518
2589
|
const wc = bundle.widgetConfig;
|
|
2519
2590
|
const t = DEFAULT_STRINGS;
|
|
2520
2591
|
const requiredQty = bundle.minQuantity || 1;
|
|
2521
|
-
const rulesMap = bundle.productRules
|
|
2592
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
2522
2593
|
const adapted = adaptProducts(bundle);
|
|
2523
|
-
const eligibleProducts = adapted.map((p) => ({
|
|
2594
|
+
const eligibleProducts = adapted.map((p, i) => ({
|
|
2524
2595
|
id: Number(p.productId),
|
|
2525
2596
|
title: p.title ?? "",
|
|
2526
2597
|
url: p.url ?? null,
|
|
2527
|
-
type: "",
|
|
2598
|
+
type: bundle.products[i]?.productType ?? "",
|
|
2528
2599
|
featuredImage: p.featuredImage ?? null,
|
|
2529
2600
|
available: p.variants.some((v) => v.available),
|
|
2530
2601
|
optionNames: p.optionNames,
|
|
@@ -2569,16 +2640,21 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2569
2640
|
const overlay = buildPickerModal({
|
|
2570
2641
|
bundleGid: bundle.id,
|
|
2571
2642
|
domId: bundle.id.replace(/\D/g, ""),
|
|
2572
|
-
showSearch:
|
|
2573
|
-
showTypeFilters: false,
|
|
2643
|
+
showSearch: wc.showSearch !== false,
|
|
2644
|
+
showTypeFilters: wc.mixMatchShowTypeFilters !== false,
|
|
2574
2645
|
segmentCount: requiredQty,
|
|
2575
2646
|
wizard: false
|
|
2576
2647
|
});
|
|
2577
2648
|
shell.root.appendChild(overlay);
|
|
2649
|
+
const currentIdx = currentProductHandle ? bundle.products.findIndex((p) => p.handle === currentProductHandle) : -1;
|
|
2578
2650
|
const selectedItems = seedSelection(
|
|
2579
2651
|
eligibleProducts,
|
|
2580
2652
|
rulesMap,
|
|
2581
|
-
requiredQty
|
|
2653
|
+
requiredQty,
|
|
2654
|
+
{
|
|
2655
|
+
enabled: !!currentProductHandle,
|
|
2656
|
+
currentProductId: currentIdx !== -1 ? eligibleProducts[currentIdx]?.id ?? null : null
|
|
2657
|
+
}
|
|
2582
2658
|
);
|
|
2583
2659
|
const rowCapacity = {
|
|
2584
2660
|
remainingSpots: () => remainingUnits(selectedItems, requiredQty),
|
|
@@ -2615,7 +2691,13 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2615
2691
|
shell.root,
|
|
2616
2692
|
selectedItems,
|
|
2617
2693
|
requiredQty,
|
|
2618
|
-
|
|
2694
|
+
// The shared updater reads the theme data-block shape: discount fields
|
|
2695
|
+
// flat, not nested under discountConfig. Passing the bundle itself left
|
|
2696
|
+
// `discountType` undefined, so the summary never painted.
|
|
2697
|
+
{
|
|
2698
|
+
discountType: bundle.discountConfig.discountType,
|
|
2699
|
+
discountValue: bundle.discountConfig.discountValue
|
|
2700
|
+
},
|
|
2619
2701
|
(total, type, value) => calculateDiscount(total, type, value),
|
|
2620
2702
|
updatePricing2(formatMoney)
|
|
2621
2703
|
);
|
|
@@ -2628,7 +2710,7 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2628
2710
|
rulesMap,
|
|
2629
2711
|
eligibleProducts,
|
|
2630
2712
|
requiredQty,
|
|
2631
|
-
showQtySelector:
|
|
2713
|
+
showQtySelector: wc.mixMatchShowQuantitySelector !== false,
|
|
2632
2714
|
items: selectedItems,
|
|
2633
2715
|
formatMoney,
|
|
2634
2716
|
rowCapacity,
|
|
@@ -2639,7 +2721,9 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2639
2721
|
onSelectionChanged: updateAll,
|
|
2640
2722
|
addSelection: (item) => {
|
|
2641
2723
|
if (totalUnits(selectedItems) + (item.quantity || 1) > requiredQty) return;
|
|
2642
|
-
const
|
|
2724
|
+
const vMax = variantRuleFor(item.variantId, rulesMap)?.max;
|
|
2725
|
+
let headroom = ruleFor(item.productId, rulesMap).max - productUnitsInOtherSlots(selectedItems, item.productId, item.variantId);
|
|
2726
|
+
if (typeof vMax === "number") headroom = Math.min(headroom, vMax);
|
|
2643
2727
|
if ((item.quantity || 1) > headroom) return;
|
|
2644
2728
|
selectedItems.push(item);
|
|
2645
2729
|
updateAll();
|
|
@@ -2689,6 +2773,13 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2689
2773
|
onCleanup?.(bindDropdowns(shell.root));
|
|
2690
2774
|
}
|
|
2691
2775
|
|
|
2776
|
+
// src/renderers/volume.ts
|
|
2777
|
+
import {
|
|
2778
|
+
bestValueTierIndex,
|
|
2779
|
+
resolveDefaultTierIndex,
|
|
2780
|
+
resolvePopularTierIndex
|
|
2781
|
+
} from "@lime-bundles/core";
|
|
2782
|
+
|
|
2692
2783
|
// ../render/src/volume/pricing.ts
|
|
2693
2784
|
function calcTierPrice(basePrice, discountType, tierEl) {
|
|
2694
2785
|
if (discountType === "fixed_amount") {
|
|
@@ -2717,6 +2808,23 @@ function formatItemCount(qty, translations) {
|
|
|
2717
2808
|
const tpl = translations.itemCount[form] || translations.itemCount.other || "__COUNT__";
|
|
2718
2809
|
return " (" + tpl.split("__COUNT__").join(String(qty)) + ")";
|
|
2719
2810
|
}
|
|
2811
|
+
function updateAllTierPrices(allTiers, basePrice, discountType, formatMoney) {
|
|
2812
|
+
for (let i = 0; i < allTiers.length; i++) {
|
|
2813
|
+
const tierEl = allTiers[i];
|
|
2814
|
+
const priceEachEl = tierEl.querySelector(
|
|
2815
|
+
"[data-tier-price-each]"
|
|
2816
|
+
);
|
|
2817
|
+
if (priceEachEl) {
|
|
2818
|
+
priceEachEl.textContent = formatMoney(
|
|
2819
|
+
calcTierPrice(basePrice, discountType, tierEl)
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
const compareEl = tierEl.querySelector(
|
|
2823
|
+
".lb-volume__tier-compare"
|
|
2824
|
+
);
|
|
2825
|
+
if (compareEl) compareEl.textContent = formatMoney(basePrice);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2720
2828
|
function selectTier(container, allTiers, basePrice, discountType, index, translations, formatMoney) {
|
|
2721
2829
|
for (let i = 0; i < allTiers.length; i++) {
|
|
2722
2830
|
allTiers[i].setAttribute("aria-checked", i === index ? "true" : "false");
|
|
@@ -2774,8 +2882,17 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
|
|
|
2774
2882
|
const discountType = bundle.discountConfig.discountType;
|
|
2775
2883
|
const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
|
|
2776
2884
|
const t = DEFAULT_STRINGS;
|
|
2777
|
-
const
|
|
2778
|
-
const defaultIdx =
|
|
2885
|
+
const bestIdx = bestValueTierIndex(bundle.volumeTiers, discountType);
|
|
2886
|
+
const defaultIdx = resolveDefaultTierIndex(
|
|
2887
|
+
wc.defaultTier,
|
|
2888
|
+
bundle.volumeTiers.length,
|
|
2889
|
+
bestIdx
|
|
2890
|
+
);
|
|
2891
|
+
const popularIdx = resolvePopularTierIndex(
|
|
2892
|
+
wc.popularBadge?.tierIndex,
|
|
2893
|
+
wc.popularBadge?.visible !== false,
|
|
2894
|
+
bestIdx
|
|
2895
|
+
);
|
|
2779
2896
|
const tiers = bundle.volumeTiers.map((tier, i) => {
|
|
2780
2897
|
const percent = discountType === "fixed_amount" ? 0 : Math.round(tier.percentage ?? 0);
|
|
2781
2898
|
const amountCents = discountType === "fixed_amount" ? Math.round((tier.amount ?? 0) * 100) : 0;
|
|
@@ -2796,16 +2913,21 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
|
|
|
2796
2913
|
title: bundle.title,
|
|
2797
2914
|
subtitle: bundle.description,
|
|
2798
2915
|
summaryLabel: "Total",
|
|
2799
|
-
withItemCount:
|
|
2916
|
+
withItemCount: wc.pricing?.showItemCount !== false,
|
|
2800
2917
|
totalPriceSlot: true,
|
|
2801
2918
|
showSavingsBar: wc.savingsBar?.visible !== false,
|
|
2802
2919
|
showComparePrice: wc.pricing?.showCompareAtPrice !== false,
|
|
2803
2920
|
ctaText: wc.cta?.ctaText || t.addToCart || "Add to cart",
|
|
2804
2921
|
endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt
|
|
2805
2922
|
});
|
|
2806
|
-
const group = buildVolumeTiers(tiers, "Select quantity"
|
|
2923
|
+
const group = buildVolumeTiers(tiers, "Select quantity", {
|
|
2924
|
+
showCompare: wc.pricing?.showComparePrice !== false,
|
|
2925
|
+
showPriceEach: wc.pricing?.showPerUnitPrice !== false,
|
|
2926
|
+
unitLabel: (t.each || "each").trim()
|
|
2927
|
+
});
|
|
2807
2928
|
shell.products.replaceWith(group);
|
|
2808
2929
|
const tierEls = group.querySelectorAll("[data-tier-index]");
|
|
2930
|
+
updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
|
|
2809
2931
|
let selectedIndex = defaultIdx;
|
|
2810
2932
|
const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
|
|
2811
2933
|
group.addEventListener("click", (e) => {
|
|
@@ -2955,6 +3077,13 @@ function buildBogoCartItems(sides, percent, buyQty, getQty) {
|
|
|
2955
3077
|
}
|
|
2956
3078
|
|
|
2957
3079
|
// src/renderers/bogo.ts
|
|
3080
|
+
function buildBogoPlus() {
|
|
3081
|
+
const plus = document.createElement("div");
|
|
3082
|
+
plus.className = "lb-bogo__plus";
|
|
3083
|
+
plus.setAttribute("aria-hidden", "true");
|
|
3084
|
+
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
|
+
return plus;
|
|
3086
|
+
}
|
|
2958
3087
|
function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
|
|
2959
3088
|
const wc = bundle.widgetConfig;
|
|
2960
3089
|
const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
|
|
@@ -3003,6 +3132,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
|
|
|
3003
3132
|
const badgeText = percent >= 100 ? "Free" : percent + "% off";
|
|
3004
3133
|
sides.forEach((side, i) => {
|
|
3005
3134
|
const isGet = i === 1;
|
|
3135
|
+
if (isGet) shell.products.appendChild(buildBogoPlus());
|
|
3006
3136
|
const isOos = !side.variants.some((v) => v.available);
|
|
3007
3137
|
const row = buildRowSkeleton(side, t, {
|
|
3008
3138
|
isOos,
|
|
@@ -3031,7 +3161,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
|
|
|
3031
3161
|
shell.cta.disabled = true;
|
|
3032
3162
|
const label = shell.cta.querySelector("[data-cta-label]");
|
|
3033
3163
|
if (label) {
|
|
3034
|
-
label.textContent = (t
|
|
3164
|
+
label.textContent = formatItemsOutOfStock(t, oosCount);
|
|
3035
3165
|
}
|
|
3036
3166
|
} else {
|
|
3037
3167
|
shell.cta.addEventListener("click", () => {
|
|
@@ -3102,6 +3232,7 @@ function allSatisfied(selections, steps) {
|
|
|
3102
3232
|
return true;
|
|
3103
3233
|
}
|
|
3104
3234
|
function canRemovePick2(selections, item, rulesMap) {
|
|
3235
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
3105
3236
|
const rule = ruleFor(item.productId, rulesMap);
|
|
3106
3237
|
if (!rule.required) return true;
|
|
3107
3238
|
let unitsElsewhere = 0;
|
|
@@ -3454,7 +3585,7 @@ function createWizard(deps) {
|
|
|
3454
3585
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
3455
3586
|
10
|
|
3456
3587
|
) || 1
|
|
3457
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
3588
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
3458
3589
|
const newItem = {
|
|
3459
3590
|
productId: prod.id,
|
|
3460
3591
|
variantId: selectedVariant.id,
|
|
@@ -3674,7 +3805,7 @@ function createWizard(deps) {
|
|
|
3674
3805
|
function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
|
|
3675
3806
|
const wc = bundle.widgetConfig;
|
|
3676
3807
|
const t = DEFAULT_STRINGS;
|
|
3677
|
-
const rulesMap = bundle.productRules
|
|
3808
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
3678
3809
|
const steps = bundle.steps ?? [];
|
|
3679
3810
|
if (!steps.length) return;
|
|
3680
3811
|
const quantities = {
|
|
@@ -3756,8 +3887,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
|
|
|
3756
3887
|
const overlay = buildPickerModal({
|
|
3757
3888
|
bundleGid: bundle.id,
|
|
3758
3889
|
domId: bundle.id.replace(/\D/g, ""),
|
|
3759
|
-
showSearch:
|
|
3760
|
-
showTypeFilters:
|
|
3890
|
+
showSearch: wc.showSearch !== false,
|
|
3891
|
+
showTypeFilters: wc.mixMatchShowTypeFilters !== false,
|
|
3761
3892
|
segmentCount: null,
|
|
3762
3893
|
wizard: true
|
|
3763
3894
|
});
|
|
@@ -3804,8 +3935,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
|
|
|
3804
3935
|
selections,
|
|
3805
3936
|
t,
|
|
3806
3937
|
rulesMap,
|
|
3807
|
-
showQtySelector:
|
|
3808
|
-
showTypeFilters:
|
|
3938
|
+
showQtySelector: wc.mixMatchShowQuantitySelector !== false,
|
|
3939
|
+
showTypeFilters: wc.mixMatchShowTypeFilters !== false,
|
|
3809
3940
|
formatMoney,
|
|
3810
3941
|
dropdown: void 0,
|
|
3811
3942
|
portalTarget: null,
|
|
@@ -4194,7 +4325,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4194
4325
|
toggle: present in admin \u2192 shown; absent \u2192 hidden. Styled as muted
|
|
4195
4326
|
secondary text beneath the price row so it doesn't compete visually. */
|
|
4196
4327
|
.lb-bundle-product-unit-price {
|
|
4197
|
-
|
|
4328
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4329
|
+
productList.showUnitPrice. The [hidden] rule below still wins for rows
|
|
4330
|
+
whose variant has no unit price at all. */
|
|
4331
|
+
display: var(--lb-product-unit-price-display, block);
|
|
4198
4332
|
font-size: 12px;
|
|
4199
4333
|
line-height: 16px;
|
|
4200
4334
|
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
@@ -4373,6 +4507,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4373
4507
|
cards). Quiet, theme-inherited; the fixed widget uses .lb-bundle-qty-chip in
|
|
4374
4508
|
the card's right slot instead. */
|
|
4375
4509
|
.lb-bundle-qty-inline {
|
|
4510
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4511
|
+
productList.showQuantity. The fixed widget's chip carries its own var
|
|
4512
|
+
because it is a flex box, not inline text. */
|
|
4513
|
+
display: var(--lb-product-qty-inline-display, inline);
|
|
4376
4514
|
font-size: 13px;
|
|
4377
4515
|
font-weight: 400;
|
|
4378
4516
|
color: color-mix(in srgb, var(--lb-text) 55%, transparent);
|
|
@@ -4605,6 +4743,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4605
4743
|
.lb-bundle-thumbnail[style*="--lb-row-thumb-ratio"] {
|
|
4606
4744
|
aspect-ratio: var(--lb-row-thumb-ratio);
|
|
4607
4745
|
}
|
|
4746
|
+
/* This selector outranks the \`.lb-bundle-thumbnail\` rule above, so Liquid must
|
|
4747
|
+
emit the inline var for the "original" ratio and nothing else. Emitting it
|
|
4748
|
+
unconditionally silently replaced every merchant ratio with the image's own,
|
|
4749
|
+
which looks like the setting being ignored. */
|
|
4608
4750
|
`;
|
|
4609
4751
|
var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
4610
4752
|
|
|
@@ -4654,7 +4796,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
|
4654
4796
|
min-width: 34px;
|
|
4655
4797
|
height: 28px;
|
|
4656
4798
|
padding: 0 8px;
|
|
4657
|
-
|
|
4799
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4800
|
+
productList.showQuantity. Separate from --lb-product-qty-inline-display
|
|
4801
|
+
because this chip is a flex box and the other counts are inline text. */
|
|
4802
|
+
display: var(--lb-product-qty-chip-display, inline-flex);
|
|
4658
4803
|
align-items: center;
|
|
4659
4804
|
justify-content: center;
|
|
4660
4805
|
border-radius: var(--lb-radius-sm);
|
|
@@ -6497,6 +6642,13 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6497
6642
|
buyer = void 0;
|
|
6498
6643
|
shadow;
|
|
6499
6644
|
bundles = [];
|
|
6645
|
+
/**
|
|
6646
|
+
* The product handle this embed is standing on, when one can be resolved
|
|
6647
|
+
* (attribute → meta tag → /products/<handle> path). The mix-and-match
|
|
6648
|
+
* courtesy seed uses it to pre-add the current product the way the theme
|
|
6649
|
+
* widget does; null on pages with no product context.
|
|
6650
|
+
*/
|
|
6651
|
+
currentProductHandle = null;
|
|
6500
6652
|
abortController = null;
|
|
6501
6653
|
impressionCleanups = [];
|
|
6502
6654
|
/**
|
|
@@ -6616,11 +6768,12 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6616
6768
|
try {
|
|
6617
6769
|
let bundlePromise;
|
|
6618
6770
|
let singleBundleMode = false;
|
|
6771
|
+
this.currentProductHandle = resolveProductHandle(this.productHandleAttr);
|
|
6619
6772
|
if (this.bundleGid) {
|
|
6620
6773
|
singleBundleMode = true;
|
|
6621
6774
|
bundlePromise = this.fetchSingleBundle(client, controller.signal);
|
|
6622
6775
|
} else {
|
|
6623
|
-
const handle =
|
|
6776
|
+
const handle = this.currentProductHandle;
|
|
6624
6777
|
if (!handle) {
|
|
6625
6778
|
this.teardownImpressions();
|
|
6626
6779
|
this.renderError(
|
|
@@ -6696,7 +6849,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6696
6849
|
const parsed = parseMetaobjectBundle(ref.id, ref.fields);
|
|
6697
6850
|
if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
|
|
6698
6851
|
}
|
|
6699
|
-
this.bundles = bundles;
|
|
6852
|
+
this.bundles = resolveAbTests(bundles, getVisitorId());
|
|
6700
6853
|
}
|
|
6701
6854
|
/**
|
|
6702
6855
|
* Dispatch add-to-cart with a cancelable event, then — unless a listener
|
|
@@ -6834,6 +6987,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6834
6987
|
container.setAttribute("aria-label", bundle.title);
|
|
6835
6988
|
container.setAttribute("data-bundle-type", bundle.bundleType);
|
|
6836
6989
|
container.setAttribute("data-bundle-gid", bundle.id);
|
|
6990
|
+
if (bundle.endsAt) container.setAttribute("data-ends-at", bundle.endsAt);
|
|
6837
6991
|
applyWidgetConfigVars(container, bundle.widgetConfig);
|
|
6838
6992
|
this.renderCleanups.push(trackInputMode(container));
|
|
6839
6993
|
const dispatch = (lines) => this.handleAddToCart(bundle, lines);
|
|
@@ -6854,7 +7008,8 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6854
7008
|
bundle,
|
|
6855
7009
|
this.outOfStockBehavior,
|
|
6856
7010
|
dispatch,
|
|
6857
|
-
registerCleanup
|
|
7011
|
+
registerCleanup,
|
|
7012
|
+
this.currentProductHandle
|
|
6858
7013
|
);
|
|
6859
7014
|
break;
|
|
6860
7015
|
case "volume":
|