@lime-bundles/widget 4.0.0 → 4.1.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/dist/index.cjs +88 -34
- package/dist/index.js +91 -35
- 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 +1 -1
- package/docs/react-nextjs.md +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1249,15 +1249,38 @@ function formatSubtitle(remaining, discountType, discountValue, t) {
|
|
|
1249
1249
|
|
|
1250
1250
|
// ../render/src/picker/rules.ts
|
|
1251
1251
|
var DEFAULT_RULE = { min: 1, max: 99 };
|
|
1252
|
-
function ruleFor(productId, rulesMap) {
|
|
1252
|
+
function ruleFor(productId, rulesMap, variantId) {
|
|
1253
|
+
if (variantId != null) {
|
|
1254
|
+
const vRule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1255
|
+
if (vRule && typeof vRule.min === "number" && typeof vRule.max === "number") {
|
|
1256
|
+
return vRule;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1253
1259
|
const rule = rulesMap["gid://shopify/Product/" + productId];
|
|
1254
1260
|
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1255
1261
|
return DEFAULT_RULE;
|
|
1256
1262
|
}
|
|
1257
1263
|
return rule;
|
|
1258
1264
|
}
|
|
1259
|
-
function
|
|
1260
|
-
|
|
1265
|
+
function variantRuleFor(variantId, rulesMap) {
|
|
1266
|
+
if (variantId == null) return void 0;
|
|
1267
|
+
const rule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1268
|
+
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
return rule;
|
|
1272
|
+
}
|
|
1273
|
+
function mergeRuleMaps(productRules, variantRules) {
|
|
1274
|
+
const products = productRules ?? {};
|
|
1275
|
+
if (!variantRules) return products;
|
|
1276
|
+
const keys = Object.keys(variantRules);
|
|
1277
|
+
if (keys.length === 0) return products;
|
|
1278
|
+
const merged = { ...products };
|
|
1279
|
+
for (const key of keys) merged[key] = variantRules[key];
|
|
1280
|
+
return merged;
|
|
1281
|
+
}
|
|
1282
|
+
function defaultPickQuantity(productId, rulesMap, variantId) {
|
|
1283
|
+
return ruleFor(productId, rulesMap, variantId).min;
|
|
1261
1284
|
}
|
|
1262
1285
|
function totalUnits(items) {
|
|
1263
1286
|
let qty = 0;
|
|
@@ -1277,11 +1300,9 @@ function committedQtyForVariant(items, variantId) {
|
|
|
1277
1300
|
function isRowAddable(rule, remainingSpots) {
|
|
1278
1301
|
return rule.min <= remainingSpots;
|
|
1279
1302
|
}
|
|
1280
|
-
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0) {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
ownQty + remainingSpots
|
|
1284
|
-
);
|
|
1303
|
+
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0, variantMax) {
|
|
1304
|
+
let ceiling = Math.min(rule.max - productOtherUnits, ownQty + remainingSpots);
|
|
1305
|
+
if (typeof variantMax === "number") ceiling = Math.min(ceiling, variantMax);
|
|
1285
1306
|
return typeof stock === "number" ? Math.min(ceiling, stock) : ceiling;
|
|
1286
1307
|
}
|
|
1287
1308
|
|
|
@@ -1360,7 +1381,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1360
1381
|
}
|
|
1361
1382
|
infoDiv.appendChild(titleEl);
|
|
1362
1383
|
let currentVariant = firstAvailable ?? (p.variants.length > 0 ? p.variants[0] : null);
|
|
1363
|
-
const
|
|
1384
|
+
const ruleForVariant = (v) => ruleFor(p.id, rulesMap, v ? v.id : null);
|
|
1385
|
+
const modalQty = currentVariant ? ruleForVariant(currentVariant).min : 1;
|
|
1364
1386
|
const priceEl = document.createElement("p");
|
|
1365
1387
|
priceEl.className = "lb-mix-match__modal-product-price";
|
|
1366
1388
|
priceEl.setAttribute("data-row-price", "");
|
|
@@ -1396,8 +1418,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1396
1418
|
infoDiv.appendChild(unitPriceEl);
|
|
1397
1419
|
let qtyGroup = null;
|
|
1398
1420
|
let qtyValueEl = null;
|
|
1399
|
-
let current =
|
|
1400
|
-
let effectiveMax =
|
|
1421
|
+
let current = ruleForVariant(currentVariant).min;
|
|
1422
|
+
let effectiveMax = ruleForVariant(currentVariant).max;
|
|
1401
1423
|
let paint = null;
|
|
1402
1424
|
let setBoundsForVariant = null;
|
|
1403
1425
|
if (p.available && ctx.showQtySelector) {
|
|
@@ -1419,7 +1441,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1419
1441
|
qtyValueEl.className = "lb-mix-match__qty-stepper-value";
|
|
1420
1442
|
qtyValueEl.setAttribute("data-row-qty", "");
|
|
1421
1443
|
qtyValueEl.setAttribute("aria-live", "polite");
|
|
1422
|
-
qtyValueEl.textContent = String(
|
|
1444
|
+
qtyValueEl.textContent = String(ruleForVariant(currentVariant).min);
|
|
1423
1445
|
const qtyPlusBtn = document.createElement("button");
|
|
1424
1446
|
qtyPlusBtn.type = "button";
|
|
1425
1447
|
qtyPlusBtn.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus";
|
|
@@ -1436,7 +1458,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1436
1458
|
paint = () => {
|
|
1437
1459
|
valueEl.textContent = String(current);
|
|
1438
1460
|
valueEl.setAttribute("data-qty", String(current));
|
|
1439
|
-
qtyMinusBtn.disabled = stepperDisabled || current <=
|
|
1461
|
+
qtyMinusBtn.disabled = stepperDisabled || current <= ruleForVariant(currentVariant).min;
|
|
1440
1462
|
qtyPlusBtn.disabled = stepperDisabled || current >= effectiveMax;
|
|
1441
1463
|
};
|
|
1442
1464
|
setBoundsForVariant = (variant, alreadyInBundle = 0) => {
|
|
@@ -1447,18 +1469,21 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1447
1469
|
const stock = variant ? variant.inventoryQuantity : null;
|
|
1448
1470
|
const othersInBundle = Math.max(0, alreadyInBundle - ownQty);
|
|
1449
1471
|
const stockCap = typeof stock === "number" ? Math.max(0, stock - othersInBundle) : void 0;
|
|
1472
|
+
const vMax = variantRuleFor(variantId, rulesMap)?.max;
|
|
1473
|
+
const rMin = ruleForVariant(variant).min;
|
|
1450
1474
|
effectiveMax = swapUnits > 0 ? typeof stockCap === "number" ? Math.min(swapUnits, stockCap) : swapUnits : stepperCeiling(
|
|
1451
1475
|
pRule,
|
|
1452
1476
|
capacity.remainingSpots(),
|
|
1453
1477
|
stockCap,
|
|
1454
1478
|
ownQty,
|
|
1455
|
-
otherUnits
|
|
1479
|
+
otherUnits,
|
|
1480
|
+
vMax
|
|
1456
1481
|
);
|
|
1457
|
-
if (effectiveMax <
|
|
1458
|
-
current =
|
|
1482
|
+
if (effectiveMax < rMin) {
|
|
1483
|
+
current = rMin;
|
|
1459
1484
|
} else {
|
|
1460
1485
|
if (current > effectiveMax) current = effectiveMax;
|
|
1461
|
-
if (current <
|
|
1486
|
+
if (current < rMin) current = rMin;
|
|
1462
1487
|
}
|
|
1463
1488
|
paint();
|
|
1464
1489
|
};
|
|
@@ -1469,7 +1494,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1469
1494
|
}
|
|
1470
1495
|
};
|
|
1471
1496
|
qtyMinusBtn.addEventListener("click", () => {
|
|
1472
|
-
if (current >
|
|
1497
|
+
if (current > ruleForVariant(currentVariant).min) current -= 1;
|
|
1473
1498
|
paint();
|
|
1474
1499
|
commitQtyToBundle();
|
|
1475
1500
|
});
|
|
@@ -1539,7 +1564,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1539
1564
|
}
|
|
1540
1565
|
currentVariant = v;
|
|
1541
1566
|
row.setAttribute("data-selected-variant-id", String(v.id));
|
|
1542
|
-
const resolvedQty =
|
|
1567
|
+
const resolvedQty = ruleForVariant(v).min;
|
|
1543
1568
|
priceSaleEl.textContent = formatMoney(v.price * resolvedQty);
|
|
1544
1569
|
paintRowCompare(priceCompareEl, v, resolvedQty);
|
|
1545
1570
|
if (v.unitPrice) {
|
|
@@ -1641,17 +1666,19 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1641
1666
|
if (setBoundsForVariant) {
|
|
1642
1667
|
const committed = capacity.ownQty(variant.id);
|
|
1643
1668
|
if (committed > 0) current = committed;
|
|
1644
|
-
else if (lastVariantId !== variant.id)
|
|
1669
|
+
else if (lastVariantId !== variant.id)
|
|
1670
|
+
current = ruleForVariant(variant).min;
|
|
1645
1671
|
lastVariantId = variant.id;
|
|
1646
1672
|
setBoundsForVariant(variant, already);
|
|
1647
1673
|
}
|
|
1648
1674
|
const rowSwapUnits = capacity.swapUnits(p.id, variant.id);
|
|
1649
|
-
const
|
|
1675
|
+
const vRule = ruleForVariant(variant);
|
|
1676
|
+
const inSwapState = rowSwapUnits >= vRule.min;
|
|
1650
1677
|
const remaining = capacity.remainingSpots();
|
|
1651
|
-
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(
|
|
1678
|
+
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(vRule, remaining);
|
|
1652
1679
|
if (needsSpotsEl) {
|
|
1653
1680
|
if (needsMoreSpots && remaining > 0) {
|
|
1654
|
-
needsSpotsEl.textContent = formatNeedsSpots(
|
|
1681
|
+
needsSpotsEl.textContent = formatNeedsSpots(vRule.min, t);
|
|
1655
1682
|
needsSpotsEl.hidden = false;
|
|
1656
1683
|
} else {
|
|
1657
1684
|
needsSpotsEl.hidden = true;
|
|
@@ -1688,11 +1715,11 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1688
1715
|
addBtn.classList.remove("lb-mix-match__modal-add--required");
|
|
1689
1716
|
row.classList.remove("lb-mix-match__modal-product--in-bundle");
|
|
1690
1717
|
addedBadge.hidden = true;
|
|
1691
|
-
const noStock = typeof stock === "number" ? Math.max(0, stock - already) <
|
|
1718
|
+
const noStock = typeof stock === "number" ? Math.max(0, stock - already) < vRule.min : false;
|
|
1692
1719
|
if (inSwapState) {
|
|
1693
1720
|
addBtn.disabled = noStock;
|
|
1694
1721
|
} else {
|
|
1695
|
-
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) <
|
|
1722
|
+
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) < vRule.min;
|
|
1696
1723
|
addBtn.disabled = noStock || productMaxed || needsMoreSpots || capacity.isFull();
|
|
1697
1724
|
}
|
|
1698
1725
|
}
|
|
@@ -1844,7 +1871,7 @@ function createPickerModal(deps) {
|
|
|
1844
1871
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
1845
1872
|
10
|
|
1846
1873
|
) || 1
|
|
1847
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
1874
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
1848
1875
|
const newItem = {
|
|
1849
1876
|
productId: prod.id,
|
|
1850
1877
|
variantId: selectedVariant.id,
|
|
@@ -2053,6 +2080,7 @@ function productUnitsInOtherSlots(items, productId, excludeVariantId) {
|
|
|
2053
2080
|
return qty;
|
|
2054
2081
|
}
|
|
2055
2082
|
function canRemovePick(items, item, rulesMap) {
|
|
2083
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
2056
2084
|
const rule = ruleFor(item.productId, rulesMap);
|
|
2057
2085
|
if (!rule.required) return true;
|
|
2058
2086
|
let unitsElsewhere = 0;
|
|
@@ -2317,8 +2345,17 @@ function seedSelection(eligibleProducts, rulesMap, requiredQty, courtesy) {
|
|
|
2317
2345
|
});
|
|
2318
2346
|
const required = [];
|
|
2319
2347
|
for (const p of eligibleProducts) {
|
|
2348
|
+
if (!p.available || !p.variants) continue;
|
|
2349
|
+
let seededVariant = false;
|
|
2350
|
+
for (const v3 of p.variants) {
|
|
2351
|
+
const vRule = variantRuleFor(v3.id, rulesMap);
|
|
2352
|
+
if (!vRule?.required || !v3.available) continue;
|
|
2353
|
+
required.push(pick(p, v3, Math.max(1, vRule.min)));
|
|
2354
|
+
seededVariant = true;
|
|
2355
|
+
}
|
|
2356
|
+
if (seededVariant) continue;
|
|
2320
2357
|
const rule = ruleFor(p.id, rulesMap);
|
|
2321
|
-
if (!rule.required
|
|
2358
|
+
if (!rule.required) continue;
|
|
2322
2359
|
const v2 = firstAvailable(p);
|
|
2323
2360
|
if (!v2) continue;
|
|
2324
2361
|
required.push(pick(p, v2, Math.max(1, rule.min)));
|
|
@@ -2523,7 +2560,7 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2523
2560
|
const wc = bundle.widgetConfig;
|
|
2524
2561
|
const t = DEFAULT_STRINGS;
|
|
2525
2562
|
const requiredQty = bundle.minQuantity || 1;
|
|
2526
|
-
const rulesMap = bundle.productRules
|
|
2563
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
2527
2564
|
const adapted = adaptProducts(bundle);
|
|
2528
2565
|
const eligibleProducts = adapted.map((p) => ({
|
|
2529
2566
|
id: Number(p.productId),
|
|
@@ -2644,7 +2681,9 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2644
2681
|
onSelectionChanged: updateAll,
|
|
2645
2682
|
addSelection: (item) => {
|
|
2646
2683
|
if (totalUnits(selectedItems) + (item.quantity || 1) > requiredQty) return;
|
|
2647
|
-
const
|
|
2684
|
+
const vMax = variantRuleFor(item.variantId, rulesMap)?.max;
|
|
2685
|
+
let headroom = ruleFor(item.productId, rulesMap).max - productUnitsInOtherSlots(selectedItems, item.productId, item.variantId);
|
|
2686
|
+
if (typeof vMax === "number") headroom = Math.min(headroom, vMax);
|
|
2648
2687
|
if ((item.quantity || 1) > headroom) return;
|
|
2649
2688
|
selectedItems.push(item);
|
|
2650
2689
|
updateAll();
|
|
@@ -3104,6 +3143,7 @@ function allSatisfied(selections, steps) {
|
|
|
3104
3143
|
return true;
|
|
3105
3144
|
}
|
|
3106
3145
|
function canRemovePick2(selections, item, rulesMap) {
|
|
3146
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
3107
3147
|
const rule = ruleFor(item.productId, rulesMap);
|
|
3108
3148
|
if (!rule.required) return true;
|
|
3109
3149
|
let unitsElsewhere = 0;
|
|
@@ -3456,7 +3496,7 @@ function createWizard(deps) {
|
|
|
3456
3496
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
3457
3497
|
10
|
|
3458
3498
|
) || 1
|
|
3459
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
3499
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
3460
3500
|
const newItem = {
|
|
3461
3501
|
productId: prod.id,
|
|
3462
3502
|
variantId: selectedVariant.id,
|
|
@@ -3676,7 +3716,7 @@ function createWizard(deps) {
|
|
|
3676
3716
|
function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
|
|
3677
3717
|
const wc = bundle.widgetConfig;
|
|
3678
3718
|
const t = DEFAULT_STRINGS;
|
|
3679
|
-
const rulesMap = bundle.productRules
|
|
3719
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
3680
3720
|
const steps = bundle.steps ?? [];
|
|
3681
3721
|
if (!steps.length) return;
|
|
3682
3722
|
const quantities = {
|
|
@@ -4196,7 +4236,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4196
4236
|
toggle: present in admin \u2192 shown; absent \u2192 hidden. Styled as muted
|
|
4197
4237
|
secondary text beneath the price row so it doesn't compete visually. */
|
|
4198
4238
|
.lb-bundle-product-unit-price {
|
|
4199
|
-
|
|
4239
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4240
|
+
productList.showUnitPrice. The [hidden] rule below still wins for rows
|
|
4241
|
+
whose variant has no unit price at all. */
|
|
4242
|
+
display: var(--lb-product-unit-price-display, block);
|
|
4200
4243
|
font-size: 12px;
|
|
4201
4244
|
line-height: 16px;
|
|
4202
4245
|
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
@@ -4375,6 +4418,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4375
4418
|
cards). Quiet, theme-inherited; the fixed widget uses .lb-bundle-qty-chip in
|
|
4376
4419
|
the card's right slot instead. */
|
|
4377
4420
|
.lb-bundle-qty-inline {
|
|
4421
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4422
|
+
productList.showQuantity. The fixed widget's chip carries its own var
|
|
4423
|
+
because it is a flex box, not inline text. */
|
|
4424
|
+
display: var(--lb-product-qty-inline-display, inline);
|
|
4378
4425
|
font-size: 13px;
|
|
4379
4426
|
font-weight: 400;
|
|
4380
4427
|
color: color-mix(in srgb, var(--lb-text) 55%, transparent);
|
|
@@ -4607,6 +4654,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4607
4654
|
.lb-bundle-thumbnail[style*="--lb-row-thumb-ratio"] {
|
|
4608
4655
|
aspect-ratio: var(--lb-row-thumb-ratio);
|
|
4609
4656
|
}
|
|
4657
|
+
/* This selector outranks the \`.lb-bundle-thumbnail\` rule above, so Liquid must
|
|
4658
|
+
emit the inline var for the "original" ratio and nothing else. Emitting it
|
|
4659
|
+
unconditionally silently replaced every merchant ratio with the image's own,
|
|
4660
|
+
which looks like the setting being ignored. */
|
|
4610
4661
|
`;
|
|
4611
4662
|
var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
4612
4663
|
|
|
@@ -4656,7 +4707,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
|
4656
4707
|
min-width: 34px;
|
|
4657
4708
|
height: 28px;
|
|
4658
4709
|
padding: 0 8px;
|
|
4659
|
-
|
|
4710
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4711
|
+
productList.showQuantity. Separate from --lb-product-qty-inline-display
|
|
4712
|
+
because this chip is a flex box and the other counts are inline text. */
|
|
4713
|
+
display: var(--lb-product-qty-chip-display, inline-flex);
|
|
4660
4714
|
align-items: center;
|
|
4661
4715
|
justify-content: center;
|
|
4662
4716
|
border-radius: var(--lb-radius-sm);
|
|
@@ -6698,7 +6752,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6698
6752
|
const parsed = (0, import_core5.parseMetaobjectBundle)(ref.id, ref.fields);
|
|
6699
6753
|
if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
|
|
6700
6754
|
}
|
|
6701
|
-
this.bundles = bundles;
|
|
6755
|
+
this.bundles = (0, import_core5.resolveAbTests)(bundles, (0, import_core5.getVisitorId)());
|
|
6702
6756
|
}
|
|
6703
6757
|
/**
|
|
6704
6758
|
* Dispatch add-to-cart with a cancelable event, then — unless a listener
|
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
|
|
@@ -1244,15 +1246,38 @@ function formatSubtitle(remaining, discountType, discountValue, t) {
|
|
|
1244
1246
|
|
|
1245
1247
|
// ../render/src/picker/rules.ts
|
|
1246
1248
|
var DEFAULT_RULE = { min: 1, max: 99 };
|
|
1247
|
-
function ruleFor(productId, rulesMap) {
|
|
1249
|
+
function ruleFor(productId, rulesMap, variantId) {
|
|
1250
|
+
if (variantId != null) {
|
|
1251
|
+
const vRule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1252
|
+
if (vRule && typeof vRule.min === "number" && typeof vRule.max === "number") {
|
|
1253
|
+
return vRule;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1248
1256
|
const rule = rulesMap["gid://shopify/Product/" + productId];
|
|
1249
1257
|
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1250
1258
|
return DEFAULT_RULE;
|
|
1251
1259
|
}
|
|
1252
1260
|
return rule;
|
|
1253
1261
|
}
|
|
1254
|
-
function
|
|
1255
|
-
|
|
1262
|
+
function variantRuleFor(variantId, rulesMap) {
|
|
1263
|
+
if (variantId == null) return void 0;
|
|
1264
|
+
const rule = rulesMap["gid://shopify/ProductVariant/" + variantId];
|
|
1265
|
+
if (!rule || typeof rule.min !== "number" || typeof rule.max !== "number") {
|
|
1266
|
+
return void 0;
|
|
1267
|
+
}
|
|
1268
|
+
return rule;
|
|
1269
|
+
}
|
|
1270
|
+
function mergeRuleMaps(productRules, variantRules) {
|
|
1271
|
+
const products = productRules ?? {};
|
|
1272
|
+
if (!variantRules) return products;
|
|
1273
|
+
const keys = Object.keys(variantRules);
|
|
1274
|
+
if (keys.length === 0) return products;
|
|
1275
|
+
const merged = { ...products };
|
|
1276
|
+
for (const key of keys) merged[key] = variantRules[key];
|
|
1277
|
+
return merged;
|
|
1278
|
+
}
|
|
1279
|
+
function defaultPickQuantity(productId, rulesMap, variantId) {
|
|
1280
|
+
return ruleFor(productId, rulesMap, variantId).min;
|
|
1256
1281
|
}
|
|
1257
1282
|
function totalUnits(items) {
|
|
1258
1283
|
let qty = 0;
|
|
@@ -1272,11 +1297,9 @@ function committedQtyForVariant(items, variantId) {
|
|
|
1272
1297
|
function isRowAddable(rule, remainingSpots) {
|
|
1273
1298
|
return rule.min <= remainingSpots;
|
|
1274
1299
|
}
|
|
1275
|
-
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
ownQty + remainingSpots
|
|
1279
|
-
);
|
|
1300
|
+
function stepperCeiling(rule, remainingSpots, stock, ownQty = 0, productOtherUnits = 0, variantMax) {
|
|
1301
|
+
let ceiling = Math.min(rule.max - productOtherUnits, ownQty + remainingSpots);
|
|
1302
|
+
if (typeof variantMax === "number") ceiling = Math.min(ceiling, variantMax);
|
|
1280
1303
|
return typeof stock === "number" ? Math.min(ceiling, stock) : ceiling;
|
|
1281
1304
|
}
|
|
1282
1305
|
|
|
@@ -1355,7 +1378,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1355
1378
|
}
|
|
1356
1379
|
infoDiv.appendChild(titleEl);
|
|
1357
1380
|
let currentVariant = firstAvailable ?? (p.variants.length > 0 ? p.variants[0] : null);
|
|
1358
|
-
const
|
|
1381
|
+
const ruleForVariant = (v) => ruleFor(p.id, rulesMap, v ? v.id : null);
|
|
1382
|
+
const modalQty = currentVariant ? ruleForVariant(currentVariant).min : 1;
|
|
1359
1383
|
const priceEl = document.createElement("p");
|
|
1360
1384
|
priceEl.className = "lb-mix-match__modal-product-price";
|
|
1361
1385
|
priceEl.setAttribute("data-row-price", "");
|
|
@@ -1391,8 +1415,8 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1391
1415
|
infoDiv.appendChild(unitPriceEl);
|
|
1392
1416
|
let qtyGroup = null;
|
|
1393
1417
|
let qtyValueEl = null;
|
|
1394
|
-
let current =
|
|
1395
|
-
let effectiveMax =
|
|
1418
|
+
let current = ruleForVariant(currentVariant).min;
|
|
1419
|
+
let effectiveMax = ruleForVariant(currentVariant).max;
|
|
1396
1420
|
let paint = null;
|
|
1397
1421
|
let setBoundsForVariant = null;
|
|
1398
1422
|
if (p.available && ctx.showQtySelector) {
|
|
@@ -1414,7 +1438,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1414
1438
|
qtyValueEl.className = "lb-mix-match__qty-stepper-value";
|
|
1415
1439
|
qtyValueEl.setAttribute("data-row-qty", "");
|
|
1416
1440
|
qtyValueEl.setAttribute("aria-live", "polite");
|
|
1417
|
-
qtyValueEl.textContent = String(
|
|
1441
|
+
qtyValueEl.textContent = String(ruleForVariant(currentVariant).min);
|
|
1418
1442
|
const qtyPlusBtn = document.createElement("button");
|
|
1419
1443
|
qtyPlusBtn.type = "button";
|
|
1420
1444
|
qtyPlusBtn.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus";
|
|
@@ -1431,7 +1455,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1431
1455
|
paint = () => {
|
|
1432
1456
|
valueEl.textContent = String(current);
|
|
1433
1457
|
valueEl.setAttribute("data-qty", String(current));
|
|
1434
|
-
qtyMinusBtn.disabled = stepperDisabled || current <=
|
|
1458
|
+
qtyMinusBtn.disabled = stepperDisabled || current <= ruleForVariant(currentVariant).min;
|
|
1435
1459
|
qtyPlusBtn.disabled = stepperDisabled || current >= effectiveMax;
|
|
1436
1460
|
};
|
|
1437
1461
|
setBoundsForVariant = (variant, alreadyInBundle = 0) => {
|
|
@@ -1442,18 +1466,21 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1442
1466
|
const stock = variant ? variant.inventoryQuantity : null;
|
|
1443
1467
|
const othersInBundle = Math.max(0, alreadyInBundle - ownQty);
|
|
1444
1468
|
const stockCap = typeof stock === "number" ? Math.max(0, stock - othersInBundle) : void 0;
|
|
1469
|
+
const vMax = variantRuleFor(variantId, rulesMap)?.max;
|
|
1470
|
+
const rMin = ruleForVariant(variant).min;
|
|
1445
1471
|
effectiveMax = swapUnits > 0 ? typeof stockCap === "number" ? Math.min(swapUnits, stockCap) : swapUnits : stepperCeiling(
|
|
1446
1472
|
pRule,
|
|
1447
1473
|
capacity.remainingSpots(),
|
|
1448
1474
|
stockCap,
|
|
1449
1475
|
ownQty,
|
|
1450
|
-
otherUnits
|
|
1476
|
+
otherUnits,
|
|
1477
|
+
vMax
|
|
1451
1478
|
);
|
|
1452
|
-
if (effectiveMax <
|
|
1453
|
-
current =
|
|
1479
|
+
if (effectiveMax < rMin) {
|
|
1480
|
+
current = rMin;
|
|
1454
1481
|
} else {
|
|
1455
1482
|
if (current > effectiveMax) current = effectiveMax;
|
|
1456
|
-
if (current <
|
|
1483
|
+
if (current < rMin) current = rMin;
|
|
1457
1484
|
}
|
|
1458
1485
|
paint();
|
|
1459
1486
|
};
|
|
@@ -1464,7 +1491,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1464
1491
|
}
|
|
1465
1492
|
};
|
|
1466
1493
|
qtyMinusBtn.addEventListener("click", () => {
|
|
1467
|
-
if (current >
|
|
1494
|
+
if (current > ruleForVariant(currentVariant).min) current -= 1;
|
|
1468
1495
|
paint();
|
|
1469
1496
|
commitQtyToBundle();
|
|
1470
1497
|
});
|
|
@@ -1534,7 +1561,7 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1534
1561
|
}
|
|
1535
1562
|
currentVariant = v;
|
|
1536
1563
|
row.setAttribute("data-selected-variant-id", String(v.id));
|
|
1537
|
-
const resolvedQty =
|
|
1564
|
+
const resolvedQty = ruleForVariant(v).min;
|
|
1538
1565
|
priceSaleEl.textContent = formatMoney(v.price * resolvedQty);
|
|
1539
1566
|
paintRowCompare(priceCompareEl, v, resolvedQty);
|
|
1540
1567
|
if (v.unitPrice) {
|
|
@@ -1636,17 +1663,19 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1636
1663
|
if (setBoundsForVariant) {
|
|
1637
1664
|
const committed = capacity.ownQty(variant.id);
|
|
1638
1665
|
if (committed > 0) current = committed;
|
|
1639
|
-
else if (lastVariantId !== variant.id)
|
|
1666
|
+
else if (lastVariantId !== variant.id)
|
|
1667
|
+
current = ruleForVariant(variant).min;
|
|
1640
1668
|
lastVariantId = variant.id;
|
|
1641
1669
|
setBoundsForVariant(variant, already);
|
|
1642
1670
|
}
|
|
1643
1671
|
const rowSwapUnits = capacity.swapUnits(p.id, variant.id);
|
|
1644
|
-
const
|
|
1672
|
+
const vRule = ruleForVariant(variant);
|
|
1673
|
+
const inSwapState = rowSwapUnits >= vRule.min;
|
|
1645
1674
|
const remaining = capacity.remainingSpots();
|
|
1646
|
-
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(
|
|
1675
|
+
const needsMoreSpots = !inBundle && !inSwapState && !isRowAddable(vRule, remaining);
|
|
1647
1676
|
if (needsSpotsEl) {
|
|
1648
1677
|
if (needsMoreSpots && remaining > 0) {
|
|
1649
|
-
needsSpotsEl.textContent = formatNeedsSpots(
|
|
1678
|
+
needsSpotsEl.textContent = formatNeedsSpots(vRule.min, t);
|
|
1650
1679
|
needsSpotsEl.hidden = false;
|
|
1651
1680
|
} else {
|
|
1652
1681
|
needsSpotsEl.hidden = true;
|
|
@@ -1683,11 +1712,11 @@ function buildPickerRow(p, rowIndex, ctx) {
|
|
|
1683
1712
|
addBtn.classList.remove("lb-mix-match__modal-add--required");
|
|
1684
1713
|
row.classList.remove("lb-mix-match__modal-product--in-bundle");
|
|
1685
1714
|
addedBadge.hidden = true;
|
|
1686
|
-
const noStock = typeof stock === "number" ? Math.max(0, stock - already) <
|
|
1715
|
+
const noStock = typeof stock === "number" ? Math.max(0, stock - already) < vRule.min : false;
|
|
1687
1716
|
if (inSwapState) {
|
|
1688
1717
|
addBtn.disabled = noStock;
|
|
1689
1718
|
} else {
|
|
1690
|
-
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) <
|
|
1719
|
+
const productMaxed = pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) < vRule.min;
|
|
1691
1720
|
addBtn.disabled = noStock || productMaxed || needsMoreSpots || capacity.isFull();
|
|
1692
1721
|
}
|
|
1693
1722
|
}
|
|
@@ -1839,7 +1868,7 @@ function createPickerModal(deps) {
|
|
|
1839
1868
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
1840
1869
|
10
|
|
1841
1870
|
) || 1
|
|
1842
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
1871
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
1843
1872
|
const newItem = {
|
|
1844
1873
|
productId: prod.id,
|
|
1845
1874
|
variantId: selectedVariant.id,
|
|
@@ -2048,6 +2077,7 @@ function productUnitsInOtherSlots(items, productId, excludeVariantId) {
|
|
|
2048
2077
|
return qty;
|
|
2049
2078
|
}
|
|
2050
2079
|
function canRemovePick(items, item, rulesMap) {
|
|
2080
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
2051
2081
|
const rule = ruleFor(item.productId, rulesMap);
|
|
2052
2082
|
if (!rule.required) return true;
|
|
2053
2083
|
let unitsElsewhere = 0;
|
|
@@ -2312,8 +2342,17 @@ function seedSelection(eligibleProducts, rulesMap, requiredQty, courtesy) {
|
|
|
2312
2342
|
});
|
|
2313
2343
|
const required = [];
|
|
2314
2344
|
for (const p of eligibleProducts) {
|
|
2345
|
+
if (!p.available || !p.variants) continue;
|
|
2346
|
+
let seededVariant = false;
|
|
2347
|
+
for (const v3 of p.variants) {
|
|
2348
|
+
const vRule = variantRuleFor(v3.id, rulesMap);
|
|
2349
|
+
if (!vRule?.required || !v3.available) continue;
|
|
2350
|
+
required.push(pick(p, v3, Math.max(1, vRule.min)));
|
|
2351
|
+
seededVariant = true;
|
|
2352
|
+
}
|
|
2353
|
+
if (seededVariant) continue;
|
|
2315
2354
|
const rule = ruleFor(p.id, rulesMap);
|
|
2316
|
-
if (!rule.required
|
|
2355
|
+
if (!rule.required) continue;
|
|
2317
2356
|
const v2 = firstAvailable(p);
|
|
2318
2357
|
if (!v2) continue;
|
|
2319
2358
|
required.push(pick(p, v2, Math.max(1, rule.min)));
|
|
@@ -2518,7 +2557,7 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2518
2557
|
const wc = bundle.widgetConfig;
|
|
2519
2558
|
const t = DEFAULT_STRINGS;
|
|
2520
2559
|
const requiredQty = bundle.minQuantity || 1;
|
|
2521
|
-
const rulesMap = bundle.productRules
|
|
2560
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
2522
2561
|
const adapted = adaptProducts(bundle);
|
|
2523
2562
|
const eligibleProducts = adapted.map((p) => ({
|
|
2524
2563
|
id: Number(p.productId),
|
|
@@ -2639,7 +2678,9 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
|
|
|
2639
2678
|
onSelectionChanged: updateAll,
|
|
2640
2679
|
addSelection: (item) => {
|
|
2641
2680
|
if (totalUnits(selectedItems) + (item.quantity || 1) > requiredQty) return;
|
|
2642
|
-
const
|
|
2681
|
+
const vMax = variantRuleFor(item.variantId, rulesMap)?.max;
|
|
2682
|
+
let headroom = ruleFor(item.productId, rulesMap).max - productUnitsInOtherSlots(selectedItems, item.productId, item.variantId);
|
|
2683
|
+
if (typeof vMax === "number") headroom = Math.min(headroom, vMax);
|
|
2643
2684
|
if ((item.quantity || 1) > headroom) return;
|
|
2644
2685
|
selectedItems.push(item);
|
|
2645
2686
|
updateAll();
|
|
@@ -3102,6 +3143,7 @@ function allSatisfied(selections, steps) {
|
|
|
3102
3143
|
return true;
|
|
3103
3144
|
}
|
|
3104
3145
|
function canRemovePick2(selections, item, rulesMap) {
|
|
3146
|
+
if (variantRuleFor(item.variantId, rulesMap)?.required) return false;
|
|
3105
3147
|
const rule = ruleFor(item.productId, rulesMap);
|
|
3106
3148
|
if (!rule.required) return true;
|
|
3107
3149
|
let unitsElsewhere = 0;
|
|
@@ -3454,7 +3496,7 @@ function createWizard(deps) {
|
|
|
3454
3496
|
rowQtyEl.getAttribute("data-qty") || rowQtyEl.textContent || "",
|
|
3455
3497
|
10
|
|
3456
3498
|
) || 1
|
|
3457
|
-
) : defaultPickQuantity(prod.id, deps.rulesMap);
|
|
3499
|
+
) : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);
|
|
3458
3500
|
const newItem = {
|
|
3459
3501
|
productId: prod.id,
|
|
3460
3502
|
variantId: selectedVariant.id,
|
|
@@ -3674,7 +3716,7 @@ function createWizard(deps) {
|
|
|
3674
3716
|
function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
|
|
3675
3717
|
const wc = bundle.widgetConfig;
|
|
3676
3718
|
const t = DEFAULT_STRINGS;
|
|
3677
|
-
const rulesMap = bundle.productRules
|
|
3719
|
+
const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
|
|
3678
3720
|
const steps = bundle.steps ?? [];
|
|
3679
3721
|
if (!steps.length) return;
|
|
3680
3722
|
const quantities = {
|
|
@@ -4194,7 +4236,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4194
4236
|
toggle: present in admin \u2192 shown; absent \u2192 hidden. Styled as muted
|
|
4195
4237
|
secondary text beneath the price row so it doesn't compete visually. */
|
|
4196
4238
|
.lb-bundle-product-unit-price {
|
|
4197
|
-
|
|
4239
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4240
|
+
productList.showUnitPrice. The [hidden] rule below still wins for rows
|
|
4241
|
+
whose variant has no unit price at all. */
|
|
4242
|
+
display: var(--lb-product-unit-price-display, block);
|
|
4198
4243
|
font-size: 12px;
|
|
4199
4244
|
line-height: 16px;
|
|
4200
4245
|
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
@@ -4373,6 +4418,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4373
4418
|
cards). Quiet, theme-inherited; the fixed widget uses .lb-bundle-qty-chip in
|
|
4374
4419
|
the card's right slot instead. */
|
|
4375
4420
|
.lb-bundle-qty-inline {
|
|
4421
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4422
|
+
productList.showQuantity. The fixed widget's chip carries its own var
|
|
4423
|
+
because it is a flex box, not inline text. */
|
|
4424
|
+
display: var(--lb-product-qty-inline-display, inline);
|
|
4376
4425
|
font-size: 13px;
|
|
4377
4426
|
font-weight: 400;
|
|
4378
4427
|
color: color-mix(in srgb, var(--lb-text) 55%, transparent);
|
|
@@ -4605,6 +4654,10 @@ a.lb-bundle-product-name:focus-visible {
|
|
|
4605
4654
|
.lb-bundle-thumbnail[style*="--lb-row-thumb-ratio"] {
|
|
4606
4655
|
aspect-ratio: var(--lb-row-thumb-ratio);
|
|
4607
4656
|
}
|
|
4657
|
+
/* This selector outranks the \`.lb-bundle-thumbnail\` rule above, so Liquid must
|
|
4658
|
+
emit the inline var for the "original" ratio and nothing else. Emitting it
|
|
4659
|
+
unconditionally silently replaced every merchant ratio with the image's own,
|
|
4660
|
+
which looks like the setting being ignored. */
|
|
4608
4661
|
`;
|
|
4609
4662
|
var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
4610
4663
|
|
|
@@ -4654,7 +4707,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
|
4654
4707
|
min-width: 34px;
|
|
4655
4708
|
height: 28px;
|
|
4656
4709
|
padding: 0 8px;
|
|
4657
|
-
|
|
4710
|
+
/* Fallback is the "on" branch; Liquid sets 'none' when the merchant disables
|
|
4711
|
+
productList.showQuantity. Separate from --lb-product-qty-inline-display
|
|
4712
|
+
because this chip is a flex box and the other counts are inline text. */
|
|
4713
|
+
display: var(--lb-product-qty-chip-display, inline-flex);
|
|
4658
4714
|
align-items: center;
|
|
4659
4715
|
justify-content: center;
|
|
4660
4716
|
border-radius: var(--lb-radius-sm);
|
|
@@ -6696,7 +6752,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6696
6752
|
const parsed = parseMetaobjectBundle(ref.id, ref.fields);
|
|
6697
6753
|
if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
|
|
6698
6754
|
}
|
|
6699
|
-
this.bundles = bundles;
|
|
6755
|
+
this.bundles = resolveAbTests(bundles, getVisitorId());
|
|
6700
6756
|
}
|
|
6701
6757
|
/**
|
|
6702
6758
|
* Dispatch add-to-cart with a cancelable event, then — unless a listener
|