@lime-bundles/widget 4.1.0 → 4.2.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 CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
28
  // src/lime-bundle.ts
29
- var import_core5 = require("@lime-bundles/core");
29
+ var import_core6 = require("@lime-bundles/core");
30
30
 
31
31
  // ../render/src/adapt.ts
32
32
  var import_core = require("@lime-bundles/core");
@@ -52,7 +52,10 @@ function adaptVariant(v, requiredQty, fallbackCurrency) {
52
52
  price: toCents(v.price?.amount),
53
53
  compareAtPrice: toCents(v.compareAtPrice?.amount),
54
54
  unitPrice: (0, import_core.formatUnitPrice)(v.unitPrice, v.unitPriceMeasurement, fallbackCurrency),
55
- image: v.image?.url ?? null
55
+ image: v.image?.url ?? null,
56
+ // Carried so the picker's stepper cap and cross-slot stock guard can
57
+ // bind on the headless surface; dropping it here left them capless.
58
+ inventoryQuantity: (0, import_core.stockCapForVariant)(v)
56
59
  };
57
60
  }
58
61
  function adaptProduct(product, bundleId, opts) {
@@ -107,6 +110,14 @@ function findVariant(product, variantId) {
107
110
  }
108
111
  return product.variants[0];
109
112
  }
113
+ function resolveSellableVariant(product) {
114
+ const seeded = findVariant(product, product.selectedVariantId);
115
+ if (seeded && seeded.available !== false) return seeded;
116
+ for (let i = 0; i < product.variants.length; i++) {
117
+ if (product.variants[i].available !== false) return product.variants[i];
118
+ }
119
+ return null;
120
+ }
110
121
  function findVariantByOptions(product, optionValues) {
111
122
  if (!product.variants || !optionValues) return null;
112
123
  for (let i = 0; i < product.variants.length; i++) {
@@ -511,7 +522,7 @@ function buildWidgetShell(typeClass, opts) {
511
522
  "data-countdown": ""
512
523
  });
513
524
  const label2 = box("span", "lb-bundle-countdown-label");
514
- label2.textContent = opts.countdownLabel ?? "Ends in";
525
+ label2.textContent = opts.countdownLabel ?? "Limited time offer";
515
526
  countdown.appendChild(label2);
516
527
  countdown.appendChild(
517
528
  box("span", "lb-bundle-countdown-timer", {
@@ -523,10 +534,12 @@ function buildWidgetShell(typeClass, opts) {
523
534
  const products = box("div", typeClass + "__products lb-edge-fade");
524
535
  root.appendChild(products);
525
536
  root.appendChild(box("div", "lb-bundle-divider"));
526
- const summary = box("div", "lb-bundle-summary");
537
+ const summary = box("div", "lb-bundle-summary", {
538
+ "data-pricing-section": ""
539
+ });
527
540
  const text = box("div", "lb-bundle-summary__text");
528
541
  const label = box("span", "lb-bundle-summary__label");
529
- label.textContent = opts.summaryLabel ?? "Bundle price";
542
+ label.textContent = opts.summaryLabel ?? "Bundle total";
530
543
  text.appendChild(label);
531
544
  if (opts.withItemCount) {
532
545
  label.appendChild(box("span", "", { "data-item-count": "" }));
@@ -557,6 +570,7 @@ function buildWidgetShell(typeClass, opts) {
557
570
  summary.appendChild(prices);
558
571
  root.appendChild(summary);
559
572
  const cta = box("button", "lb-bundle-cta", {
573
+ "data-cta-text": opts.ctaText,
560
574
  type: "button",
561
575
  "data-add-bundle": ""
562
576
  });
@@ -575,7 +589,9 @@ function buildWidgetShell(typeClass, opts) {
575
589
  );
576
590
  return { root, products, cta };
577
591
  }
578
- function buildVolumeTiers(tiers, groupLabel) {
592
+ function buildVolumeTiers(tiers, groupLabel, opts = {}) {
593
+ const showCompare = opts.showCompare !== false;
594
+ const showPriceEach = opts.showPriceEach !== false;
579
595
  const group = box("div", "lb-volume__tiers lb-edge-fade", {
580
596
  role: "radiogroup",
581
597
  "aria-label": groupLabel,
@@ -600,8 +616,17 @@ function buildVolumeTiers(tiers, groupLabel) {
600
616
  label.textContent = tier.label;
601
617
  info.appendChild(label);
602
618
  const price = box("span", "lb-volume__tier-price");
603
- price.appendChild(box("span", "lb-volume__tier-compare"));
604
- price.appendChild(box("span", "", { "data-tier-price-each": "" }));
619
+ if (showCompare && (tier.percent > 0 || tier.amountCents > 0)) {
620
+ price.appendChild(box("span", "lb-volume__tier-compare"));
621
+ }
622
+ if (showPriceEach) {
623
+ price.appendChild(box("span", "", { "data-tier-price-each": "" }));
624
+ if (opts.unitLabel) {
625
+ const unit = box("span", "lb-volume__tier-unit");
626
+ unit.textContent = opts.unitLabel;
627
+ price.appendChild(unit);
628
+ }
629
+ }
605
630
  info.appendChild(price);
606
631
  el2.appendChild(info);
607
632
  if (tier.badgeLabel || tier.savingsLabel) {
@@ -710,7 +735,10 @@ var DEFAULT_STRINGS = {
710
735
  required: "Required",
711
736
  soldOut: "Sold out",
712
737
  outOfStock: "Out of stock",
713
- itemsOutOfStock: "__COUNT__ items out of stock",
738
+ itemsOutOfStock: {
739
+ one: "__COUNT__ item out of stock",
740
+ other: "__COUNT__ items out of stock"
741
+ },
714
742
  quantity: "Quantity",
715
743
  increaseQuantity: "Increase quantity",
716
744
  decreaseQuantity: "Decrease quantity",
@@ -730,11 +758,26 @@ var DEFAULT_STRINGS = {
730
758
  stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
731
759
  each: " each",
732
760
  buyQty: "Buy __COUNT__",
761
+ itemCount: {
762
+ one: "__COUNT__ item",
763
+ other: "__COUNT__ items"
764
+ },
733
765
  needsSpots: {
734
766
  one: "Needs __COUNT__ spot",
735
767
  other: "Needs __COUNT__ spots"
736
768
  }
737
769
  };
770
+ function formatItemsOutOfStock(t, count) {
771
+ const raw = t.itemsOutOfStock;
772
+ let tpl;
773
+ if (raw && typeof raw === "object") {
774
+ const form = new Intl.PluralRules(t.locale || "en").select(count);
775
+ tpl = raw[form] || raw.other;
776
+ } else {
777
+ tpl = raw;
778
+ }
779
+ return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
780
+ }
738
781
 
739
782
  // ../render/src/dropdown/bind.ts
740
783
  var import_core2 = require("@lime-bundles/core");
@@ -1145,12 +1188,11 @@ function bindDropdowns(root) {
1145
1188
  }
1146
1189
 
1147
1190
  // src/renderers/fixed.ts
1148
- function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
1191
+ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
1149
1192
  const wc = bundle.widgetConfig;
1150
1193
  const products = adaptProducts(bundle);
1151
1194
  if (!products.length) return;
1152
1195
  const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));
1153
- if (outOfStockBehavior === "hide" && oosProducts.length > 0) return;
1154
1196
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
1155
1197
  const formatMoney = intlFormatMoney(currency);
1156
1198
  const t = DEFAULT_STRINGS;
@@ -1175,7 +1217,9 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
1175
1217
  });
1176
1218
  shell.products.appendChild(row);
1177
1219
  if (isOos) continue;
1178
- const selected = findVariant(product, product.selectedVariantId);
1220
+ const sellable = resolveSellableVariant(product);
1221
+ if (sellable) product.selectedVariantId = sellable.id;
1222
+ const selected = sellable ?? findVariant(product, product.selectedVariantId);
1179
1223
  hydrateRowControls(row, product, selected);
1180
1224
  applyVariant(row, selected, product, formatMoney);
1181
1225
  bindVariantSelects(
@@ -1191,7 +1235,7 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
1191
1235
  shell.cta.disabled = true;
1192
1236
  const label = shell.cta.querySelector("[data-cta-label]");
1193
1237
  if (label) {
1194
- label.textContent = (t.itemsOutOfStock || "__COUNT__ items out of stock").split("__COUNT__").join(String(oosProducts.length));
1238
+ label.textContent = formatItemsOutOfStock(t, oosProducts.length);
1195
1239
  }
1196
1240
  } else {
1197
1241
  shell.cta.addEventListener("click", () => {
@@ -1770,6 +1814,17 @@ function unlock() {
1770
1814
  window.scrollTo(0, savedY);
1771
1815
  }
1772
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
+
1773
1828
  // ../render/src/mix-match/modal.ts
1774
1829
  var ALL_TYPES = "__all__";
1775
1830
  var CLOSE_TRANSITION_MS = 300;
@@ -1994,12 +2049,22 @@ function createPickerModal(deps) {
1994
2049
  syncActiveFilterPill();
1995
2050
  if (searchInput) searchInput.value = "";
1996
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
+ }
1997
2062
  overlay.style.display = "";
1998
2063
  void overlay.offsetHeight;
1999
2064
  overlay.classList.add("lb-mix-match__modal-overlay--open");
2000
2065
  lock();
2001
2066
  setTimeout(() => {
2002
- if (searchInput) searchInput.focus();
2067
+ if (closeBtn) closeBtn.focus();
2003
2068
  else modalDialog?.focus();
2004
2069
  }, 50);
2005
2070
  }
@@ -2556,17 +2621,17 @@ function buildPickerModal(opts) {
2556
2621
  }
2557
2622
 
2558
2623
  // src/renderers/mix-match.ts
2559
- function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
2624
+ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, currentProductHandle) {
2560
2625
  const wc = bundle.widgetConfig;
2561
2626
  const t = DEFAULT_STRINGS;
2562
2627
  const requiredQty = bundle.minQuantity || 1;
2563
2628
  const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
2564
2629
  const adapted = adaptProducts(bundle);
2565
- const eligibleProducts = adapted.map((p) => ({
2630
+ const eligibleProducts = adapted.map((p, i) => ({
2566
2631
  id: Number(p.productId),
2567
2632
  title: p.title ?? "",
2568
2633
  url: p.url ?? null,
2569
- type: "",
2634
+ type: bundle.products[i]?.productType ?? "",
2570
2635
  featuredImage: p.featuredImage ?? null,
2571
2636
  available: p.variants.some((v) => v.available),
2572
2637
  optionNames: p.optionNames,
@@ -2579,12 +2644,12 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2579
2644
  compareAtPrice: v.compareAtPrice,
2580
2645
  unitPrice: v.unitPrice,
2581
2646
  image: v.image,
2582
- inventoryQuantity: null
2647
+ inventoryQuantity: v.inventoryQuantity ?? null
2583
2648
  }))
2584
2649
  }));
2585
2650
  if (!eligibleProducts.length) return;
2586
2651
  const poolUnits = eligibleProducts.filter((p) => p.available).length;
2587
- if (outOfStockBehavior === "hide" && poolUnits === 0) return;
2652
+ if (poolUnits === 0) return;
2588
2653
  const formatMoney = intlFormatMoney(
2589
2654
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
2590
2655
  );
@@ -2611,16 +2676,21 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2611
2676
  const overlay = buildPickerModal({
2612
2677
  bundleGid: bundle.id,
2613
2678
  domId: bundle.id.replace(/\D/g, ""),
2614
- showSearch: true,
2615
- showTypeFilters: false,
2679
+ showSearch: wc.showSearch !== false,
2680
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
2616
2681
  segmentCount: requiredQty,
2617
2682
  wizard: false
2618
2683
  });
2619
2684
  shell.root.appendChild(overlay);
2685
+ const currentIdx = currentProductHandle ? bundle.products.findIndex((p) => p.handle === currentProductHandle) : -1;
2620
2686
  const selectedItems = seedSelection(
2621
2687
  eligibleProducts,
2622
2688
  rulesMap,
2623
- requiredQty
2689
+ requiredQty,
2690
+ {
2691
+ enabled: !!currentProductHandle,
2692
+ currentProductId: currentIdx !== -1 ? eligibleProducts[currentIdx]?.id ?? null : null
2693
+ }
2624
2694
  );
2625
2695
  const rowCapacity = {
2626
2696
  remainingSpots: () => remainingUnits(selectedItems, requiredQty),
@@ -2657,7 +2727,13 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2657
2727
  shell.root,
2658
2728
  selectedItems,
2659
2729
  requiredQty,
2660
- bundle,
2730
+ // The shared updater reads the theme data-block shape: discount fields
2731
+ // flat, not nested under discountConfig. Passing the bundle itself left
2732
+ // `discountType` undefined, so the summary never painted.
2733
+ {
2734
+ discountType: bundle.discountConfig.discountType,
2735
+ discountValue: bundle.discountConfig.discountValue
2736
+ },
2661
2737
  (total, type, value) => (0, import_core3.calculateDiscount)(total, type, value),
2662
2738
  updatePricing2(formatMoney)
2663
2739
  );
@@ -2670,7 +2746,7 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2670
2746
  rulesMap,
2671
2747
  eligibleProducts,
2672
2748
  requiredQty,
2673
- showQtySelector: true,
2749
+ showQtySelector: wc.mixMatchShowQuantitySelector !== false,
2674
2750
  items: selectedItems,
2675
2751
  formatMoney,
2676
2752
  rowCapacity,
@@ -2733,6 +2809,47 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2733
2809
  onCleanup?.(bindDropdowns(shell.root));
2734
2810
  }
2735
2811
 
2812
+ // src/renderers/volume.ts
2813
+ var import_core4 = require("@lime-bundles/core");
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
+
2736
2853
  // ../render/src/volume/pricing.ts
2737
2854
  function calcTierPrice(basePrice, discountType, tierEl) {
2738
2855
  if (discountType === "fixed_amount") {
@@ -2761,6 +2878,23 @@ function formatItemCount(qty, translations) {
2761
2878
  const tpl = translations.itemCount[form] || translations.itemCount.other || "__COUNT__";
2762
2879
  return " (" + tpl.split("__COUNT__").join(String(qty)) + ")";
2763
2880
  }
2881
+ function updateAllTierPrices(allTiers, basePrice, discountType, formatMoney) {
2882
+ for (let i = 0; i < allTiers.length; i++) {
2883
+ const tierEl = allTiers[i];
2884
+ const priceEachEl = tierEl.querySelector(
2885
+ "[data-tier-price-each]"
2886
+ );
2887
+ if (priceEachEl) {
2888
+ priceEachEl.textContent = formatMoney(
2889
+ calcTierPrice(basePrice, discountType, tierEl)
2890
+ );
2891
+ }
2892
+ const compareEl = tierEl.querySelector(
2893
+ ".lb-volume__tier-compare"
2894
+ );
2895
+ if (compareEl) compareEl.textContent = formatMoney(basePrice);
2896
+ }
2897
+ }
2764
2898
  function selectTier(container, allTiers, basePrice, discountType, index, translations, formatMoney) {
2765
2899
  for (let i = 0; i < allTiers.length; i++) {
2766
2900
  allTiers[i].setAttribute("aria-checked", i === index ? "true" : "false");
@@ -2805,21 +2939,31 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
2805
2939
  }
2806
2940
 
2807
2941
  // src/renderers/volume.ts
2808
- function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart) {
2942
+ function renderVolumeBundle(container, bundle, onAddToCart) {
2809
2943
  const wc = bundle.widgetConfig;
2810
2944
  const product = bundle.products[0];
2811
2945
  if (!product) return;
2812
2946
  const variants = product.variants.nodes;
2813
- const pricingVariant = variants.find((v) => v.availableForSale) ?? variants[0];
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];
2814
2951
  if (!pricingVariant) return;
2815
- const anyAvailable = variants.some((v) => v.availableForSale);
2816
- if (outOfStockBehavior === "hide" && !anyAvailable) return;
2817
2952
  const basePrice = toCents(pricingVariant.price.amount);
2818
2953
  const discountType = bundle.discountConfig.discountType;
2819
2954
  const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
2820
2955
  const t = DEFAULT_STRINGS;
2821
- const popularIdx = wc.popularBadge?.visible ? Math.floor((bundle.volumeTiers.length - 1) / 2) : -1;
2822
- const defaultIdx = 0;
2956
+ const bestIdx = (0, import_core4.bestValueTierIndex)(bundle.volumeTiers, discountType);
2957
+ const defaultIdx = (0, import_core4.resolveDefaultTierIndex)(
2958
+ wc.defaultTier,
2959
+ bundle.volumeTiers.length,
2960
+ bestIdx
2961
+ );
2962
+ const popularIdx = (0, import_core4.resolvePopularTierIndex)(
2963
+ wc.popularBadge?.tierIndex,
2964
+ wc.popularBadge?.visible !== false,
2965
+ bestIdx
2966
+ );
2823
2967
  const tiers = bundle.volumeTiers.map((tier, i) => {
2824
2968
  const percent = discountType === "fixed_amount" ? 0 : Math.round(tier.percentage ?? 0);
2825
2969
  const amountCents = discountType === "fixed_amount" ? Math.round((tier.amount ?? 0) * 100) : 0;
@@ -2840,38 +2984,46 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
2840
2984
  title: bundle.title,
2841
2985
  subtitle: bundle.description,
2842
2986
  summaryLabel: "Total",
2843
- withItemCount: true,
2987
+ withItemCount: wc.pricing?.showItemCount !== false,
2844
2988
  totalPriceSlot: true,
2845
2989
  showSavingsBar: wc.savingsBar?.visible !== false,
2846
2990
  showComparePrice: wc.pricing?.showCompareAtPrice !== false,
2847
2991
  ctaText: wc.cta?.ctaText || t.addToCart || "Add to cart",
2848
2992
  endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt
2849
2993
  });
2850
- const group = buildVolumeTiers(tiers, "Select quantity");
2994
+ const group = buildVolumeTiers(tiers, "Select quantity", {
2995
+ showCompare: wc.pricing?.showComparePrice !== false,
2996
+ showPriceEach: wc.pricing?.showPerUnitPrice !== false,
2997
+ unitLabel: (t.each || "each").trim()
2998
+ });
2851
2999
  shell.products.replaceWith(group);
2852
3000
  const tierEls = group.querySelectorAll("[data-tier-index]");
2853
- let selectedIndex = defaultIdx;
3001
+ updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
3002
+ applyTierStockState(tierEls, (0, import_core4.stockCapForVariant)(pricingVariant));
3003
+ let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);
3004
+ const noTierAvailable = selectedIndex === -1;
3005
+ if (noTierAvailable) selectedIndex = defaultIdx;
2854
3006
  const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
2855
3007
  group.addEventListener("click", (e) => {
2856
3008
  const el2 = e.target.closest("[data-tier-index]");
2857
- if (!el2) return;
3009
+ if (!el2 || tierUnavailable(el2)) return;
2858
3010
  selectedIndex = parseInt(el2.getAttribute("data-tier-index") ?? "", 10) || 0;
2859
3011
  apply();
2860
3012
  });
2861
3013
  group.addEventListener("keydown", (e) => {
2862
3014
  if (e.key === "ArrowDown" || e.key === "ArrowRight") {
2863
3015
  e.preventDefault();
2864
- selectedIndex = Math.min(selectedIndex + 1, tierEls.length - 1);
3016
+ selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);
2865
3017
  apply();
2866
3018
  tierEls[selectedIndex].focus();
2867
3019
  } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
2868
3020
  e.preventDefault();
2869
- selectedIndex = Math.max(selectedIndex - 1, 0);
3021
+ selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);
2870
3022
  apply();
2871
3023
  tierEls[selectedIndex].focus();
2872
3024
  }
2873
3025
  });
2874
- if (!anyAvailable) {
3026
+ if (noTierAvailable) {
2875
3027
  shell.cta.disabled = true;
2876
3028
  const label = shell.cta.querySelector("[data-cta-label]");
2877
3029
  if (label) label.textContent = t.outOfStock || "Out of stock";
@@ -2999,7 +3151,14 @@ function buildBogoCartItems(sides, percent, buyQty, getQty) {
2999
3151
  }
3000
3152
 
3001
3153
  // src/renderers/bogo.ts
3002
- function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
3154
+ function buildBogoPlus() {
3155
+ const plus = document.createElement("div");
3156
+ plus.className = "lb-bogo__plus";
3157
+ plus.setAttribute("aria-hidden", "true");
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>';
3159
+ return plus;
3160
+ }
3161
+ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3003
3162
  const wc = bundle.widgetConfig;
3004
3163
  const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
3005
3164
  const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);
@@ -3028,7 +3187,6 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3028
3187
  }
3029
3188
  ];
3030
3189
  const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;
3031
- if (outOfStockBehavior === "hide" && oosCount > 0) return;
3032
3190
  const formatMoney = intlFormatMoney(
3033
3191
  buyProduct.priceRange.minVariantPrice.currencyCode
3034
3192
  );
@@ -3047,6 +3205,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3047
3205
  const badgeText = percent >= 100 ? "Free" : percent + "% off";
3048
3206
  sides.forEach((side, i) => {
3049
3207
  const isGet = i === 1;
3208
+ if (isGet) shell.products.appendChild(buildBogoPlus());
3050
3209
  const isOos = !side.variants.some((v) => v.available);
3051
3210
  const row = buildRowSkeleton(side, t, {
3052
3211
  isOos,
@@ -3075,7 +3234,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3075
3234
  shell.cta.disabled = true;
3076
3235
  const label = shell.cta.querySelector("[data-cta-label]");
3077
3236
  if (label) {
3078
- label.textContent = (t.itemsOutOfStock || "__COUNT__ items out of stock").split("__COUNT__").join(String(oosCount));
3237
+ label.textContent = formatItemsOutOfStock(t, oosCount);
3079
3238
  }
3080
3239
  } else {
3081
3240
  shell.cta.addEventListener("click", () => {
@@ -3095,7 +3254,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3095
3254
  }
3096
3255
 
3097
3256
  // src/renderers/multi-step.ts
3098
- var import_core4 = require("@lime-bundles/core");
3257
+ var import_core5 = require("@lime-bundles/core");
3099
3258
 
3100
3259
  // ../render/src/multi-step/i18n.ts
3101
3260
  function stepOfLabel(i, total, t) {
@@ -3460,6 +3619,14 @@ function createWizard(deps) {
3460
3619
  rows.push(row);
3461
3620
  modalList.appendChild(row.el);
3462
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
+ );
3463
3630
  deps.dropdown?.bindAll(modalList);
3464
3631
  }
3465
3632
  function onListClick(e) {
@@ -3622,6 +3789,7 @@ function createWizard(deps) {
3622
3789
  syncActiveFilterPill();
3623
3790
  if (searchInput) searchInput.value = "";
3624
3791
  filterProducts("");
3792
+ if (modalList) modalList.scrollTop = 0;
3625
3793
  buildStepSegments(stepIndex);
3626
3794
  refresh();
3627
3795
  if (modalLive) {
@@ -3645,7 +3813,7 @@ function createWizard(deps) {
3645
3813
  overlay.classList.add("lb-mix-match__modal-overlay--open");
3646
3814
  lock();
3647
3815
  setTimeout(() => {
3648
- if (searchInput) searchInput.focus();
3816
+ if (closeBtn) closeBtn.focus();
3649
3817
  else modalDialog?.focus();
3650
3818
  }, 50);
3651
3819
  }
@@ -3713,7 +3881,7 @@ function createWizard(deps) {
3713
3881
  }
3714
3882
 
3715
3883
  // src/renderers/multi-step.ts
3716
- function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
3884
+ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
3717
3885
  const wc = bundle.widgetConfig;
3718
3886
  const t = DEFAULT_STRINGS;
3719
3887
  const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
@@ -3724,7 +3892,7 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3724
3892
  variantQuantities: bundle.variantQuantities
3725
3893
  };
3726
3894
  const pools = steps.map(
3727
- (_, i) => (0, import_core4.productsForStep)(bundle, i).map((product) => {
3895
+ (_, i) => (0, import_core5.productsForStep)(bundle, i).map((product) => {
3728
3896
  const p = adaptProduct(product, bundle.id, {
3729
3897
  quantities,
3730
3898
  allowedVariantIds: null
@@ -3746,13 +3914,13 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3746
3914
  compareAtPrice: v.compareAtPrice,
3747
3915
  unitPrice: v.unitPrice,
3748
3916
  image: v.image,
3749
- inventoryQuantity: null
3917
+ inventoryQuantity: v.inventoryQuantity ?? null
3750
3918
  }))
3751
3919
  };
3752
3920
  })
3753
3921
  );
3754
- const anyPickable = pools.some((pool) => pool.some((p) => p.available));
3755
- if (outOfStockBehavior === "hide" && !anyPickable) return;
3922
+ const everyStepPickable = pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));
3923
+ if (!everyStepPickable) return;
3756
3924
  const formatMoney = intlFormatMoney(
3757
3925
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
3758
3926
  );
@@ -3798,8 +3966,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3798
3966
  const overlay = buildPickerModal({
3799
3967
  bundleGid: bundle.id,
3800
3968
  domId: bundle.id.replace(/\D/g, ""),
3801
- showSearch: true,
3802
- showTypeFilters: true,
3969
+ showSearch: wc.showSearch !== false,
3970
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
3803
3971
  segmentCount: null,
3804
3972
  wizard: true
3805
3973
  });
@@ -3834,7 +4002,7 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3834
4002
  discountType: bundle.discountConfig.discountType,
3835
4003
  discountValue: bundle.discountConfig.discountValue
3836
4004
  },
3837
- (total, type, value) => (0, import_core4.calculateDiscount)(total, type, value),
4005
+ (total, type, value) => (0, import_core5.calculateDiscount)(total, type, value),
3838
4006
  updatePricing2(formatMoney)
3839
4007
  );
3840
4008
  updateProgress2(bodyDeps);
@@ -3846,8 +4014,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3846
4014
  selections,
3847
4015
  t,
3848
4016
  rulesMap,
3849
- showQtySelector: true,
3850
- showTypeFilters: true,
4017
+ showQtySelector: wc.mixMatchShowQuantitySelector !== false,
4018
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
3851
4019
  formatMoney,
3852
4020
  dropdown: void 0,
3853
4021
  portalTarget: null,
@@ -3897,7 +4065,7 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3897
4065
  }
3898
4066
 
3899
4067
  // src/lime-bundle.ts
3900
- var import_core6 = require("@lime-bundles/core");
4068
+ var import_core7 = require("@lime-bundles/core");
3901
4069
 
3902
4070
  // src/utils/input-mode.ts
3903
4071
  var NAV_KEYS = /* @__PURE__ */ new Set([
@@ -5564,7 +5732,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5564
5732
  transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
5565
5733
  }
5566
5734
 
5567
- .lb-mix-match__modal-add:hover {
5735
+ .lb-mix-match__modal-add:hover:not(:disabled) {
5568
5736
  opacity: 0.9;
5569
5737
  }
5570
5738
 
@@ -5679,8 +5847,15 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5679
5847
  color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);
5680
5848
  }
5681
5849
 
5682
- /* "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. */
5683
5854
  .lb-mix-match__modal-done {
5855
+ display: inline-flex;
5856
+ align-items: center;
5857
+ justify-content: center;
5858
+ min-height: 40px;
5684
5859
  padding: 10px 24px;
5685
5860
  transition: opacity 0.25s ease;
5686
5861
  background: var(--lb-picker-add-bg);
@@ -5694,7 +5869,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5694
5869
  cursor: pointer;
5695
5870
  }
5696
5871
 
5697
- .lb-mix-match__modal-done:hover {
5872
+ .lb-mix-match__modal-done:hover:not(:disabled) {
5698
5873
  opacity: 0.9;
5699
5874
  }
5700
5875
 
@@ -5704,6 +5879,16 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5704
5879
  box-shadow: none;
5705
5880
  }
5706
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
+
5707
5892
  /* Hidden utility for search filtering */
5708
5893
  .lb-hidden {
5709
5894
  display: none !important;
@@ -5960,6 +6145,13 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
5960
6145
  white-space: nowrap;
5961
6146
  font-variant-numeric: tabular-nums;
5962
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
+ }
5963
6155
  `;
5964
6156
  var BUNDLE_BOGO_CSS = `/* Lime Bundles \u2014 BOGO (Buy X Get Y) bundle styles */
5965
6157
 
@@ -6128,14 +6320,23 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6128
6320
  justify-self: end;
6129
6321
  }
6130
6322
 
6131
- /* Back \u2014 secondary treatment beside the primary Next/Done (which reuse
6132
- .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. */
6133
6329
  .lb-multi-step__modal-back {
6134
- padding: 10px 16px;
6330
+ display: inline-flex;
6331
+ align-items: center;
6332
+ justify-content: center;
6333
+ min-height: 40px;
6334
+ padding: 10px 24px;
6135
6335
  background: none;
6136
- border: var(--lb-border-width) solid var(--lb-border-color);
6137
- border-radius: var(--lb-radius-sm);
6138
- color: var(--lb-text);
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);
6139
6340
  font-family: inherit;
6140
6341
  font-size: 14px;
6141
6342
  font-weight: 600;
@@ -6144,7 +6345,7 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6144
6345
  }
6145
6346
 
6146
6347
  .lb-multi-step__modal-back:hover {
6147
- background: color-mix(in srgb, var(--lb-text) 5%, transparent);
6348
+ background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);
6148
6349
  }
6149
6350
 
6150
6351
  .lb-multi-step__modal-back:focus-visible {
@@ -6553,6 +6754,13 @@ var LimeBundleElement = class extends HTMLElement {
6553
6754
  buyer = void 0;
6554
6755
  shadow;
6555
6756
  bundles = [];
6757
+ /**
6758
+ * The product handle this embed is standing on, when one can be resolved
6759
+ * (attribute → meta tag → /products/<handle> path). The mix-and-match
6760
+ * courtesy seed uses it to pre-add the current product the way the theme
6761
+ * widget does; null on pages with no product context.
6762
+ */
6763
+ currentProductHandle = null;
6556
6764
  abortController = null;
6557
6765
  impressionCleanups = [];
6558
6766
  /**
@@ -6567,12 +6775,6 @@ var LimeBundleElement = class extends HTMLElement {
6567
6775
  * `.lb-bundle-widget { ... }` reach the widget's DOM.
6568
6776
  */
6569
6777
  shopCustomCss = null;
6570
- /**
6571
- * Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
6572
- * before the first render. Defaults to `show_greyed_out` so a failed or
6573
- * missing fetch degrades to "bundles still render".
6574
- */
6575
- outOfStockBehavior = import_core5.DEFAULT_OUT_OF_STOCK_BEHAVIOR;
6576
6778
  constructor() {
6577
6779
  super();
6578
6780
  this.shadow = this.attachShadow({ mode: "open" });
@@ -6634,13 +6836,13 @@ var LimeBundleElement = class extends HTMLElement {
6634
6836
  * no buyer/country/language is set.
6635
6837
  */
6636
6838
  wrapQueryForContext(query) {
6637
- return (0, import_core5.hasInContext)({
6839
+ return (0, import_core6.hasInContext)({
6638
6840
  shopDomain: this.shopDomain,
6639
6841
  accessToken: this.storefrontToken,
6640
6842
  country: this.country,
6641
6843
  language: this.language,
6642
6844
  buyer: this.buyer
6643
- }) ? (0, import_core5.withInContext)(query) : query;
6845
+ }) ? (0, import_core6.withInContext)(query) : query;
6644
6846
  }
6645
6847
  /**
6646
6848
  * Returns true if this bundle should be hidden for the current market.
@@ -6649,7 +6851,7 @@ var LimeBundleElement = class extends HTMLElement {
6649
6851
  * marketIds doesn't include the configured market.
6650
6852
  */
6651
6853
  isMarketHidden(bundle) {
6652
- return !(0, import_core5.isVisibleInMarket)(bundle, this.marketId);
6854
+ return !(0, import_core6.isVisibleInMarket)(bundle, this.marketId);
6653
6855
  }
6654
6856
  async fetchBundle() {
6655
6857
  if (!this.shopDomain || !this.storefrontToken) {
@@ -6662,7 +6864,7 @@ var LimeBundleElement = class extends HTMLElement {
6662
6864
  const controller = new AbortController();
6663
6865
  this.abortController = controller;
6664
6866
  this.renderLoading();
6665
- const client = (0, import_core5.createStorefrontClient)({
6867
+ const client = (0, import_core6.createStorefrontClient)({
6666
6868
  shopDomain: this.shopDomain,
6667
6869
  accessToken: this.storefrontToken,
6668
6870
  country: this.country,
@@ -6672,11 +6874,12 @@ var LimeBundleElement = class extends HTMLElement {
6672
6874
  try {
6673
6875
  let bundlePromise;
6674
6876
  let singleBundleMode = false;
6877
+ this.currentProductHandle = resolveProductHandle(this.productHandleAttr);
6675
6878
  if (this.bundleGid) {
6676
6879
  singleBundleMode = true;
6677
6880
  bundlePromise = this.fetchSingleBundle(client, controller.signal);
6678
6881
  } else {
6679
- const handle = resolveProductHandle(this.productHandleAttr);
6882
+ const handle = this.currentProductHandle;
6680
6883
  if (!handle) {
6681
6884
  this.teardownImpressions();
6682
6885
  this.renderError(
@@ -6690,7 +6893,7 @@ var LimeBundleElement = class extends HTMLElement {
6690
6893
  handle
6691
6894
  );
6692
6895
  }
6693
- const shopSettingsPromise = client.query(import_core5.SHOP_SETTINGS_QUERY, void 0, {
6896
+ const shopSettingsPromise = client.query(import_core6.SHOP_SETTINGS_QUERY, void 0, {
6694
6897
  signal: controller.signal
6695
6898
  }).catch(() => null);
6696
6899
  await bundlePromise;
@@ -6703,13 +6906,10 @@ var LimeBundleElement = class extends HTMLElement {
6703
6906
  const shopSettings = await shopSettingsPromise;
6704
6907
  const customCss = shopSettings?.shop?.customCss?.value;
6705
6908
  if (customCss) {
6706
- (0, import_core5.injectCustomCss)(this.shopDomain, customCss);
6707
- const sanitized = (0, import_core5.sanitizeCustomCss)(customCss);
6909
+ (0, import_core6.injectCustomCss)(this.shopDomain, customCss);
6910
+ const sanitized = (0, import_core6.sanitizeCustomCss)(customCss);
6708
6911
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
6709
6912
  }
6710
- this.outOfStockBehavior = (0, import_core5.parseOutOfStockBehavior)(
6711
- shopSettings?.shop?.outOfStockBehavior?.value
6712
- );
6713
6913
  this.renderBundles();
6714
6914
  } catch (err) {
6715
6915
  if (controller.signal.aborted) return;
@@ -6722,7 +6922,7 @@ var LimeBundleElement = class extends HTMLElement {
6722
6922
  }
6723
6923
  async fetchSingleBundle(client, signal) {
6724
6924
  const data = await client.query(
6725
- this.wrapQueryForContext(import_core5.BUNDLE_METAOBJECT_QUERY),
6925
+ this.wrapQueryForContext(import_core6.BUNDLE_METAOBJECT_QUERY),
6726
6926
  { id: this.bundleGid },
6727
6927
  { signal }
6728
6928
  );
@@ -6730,7 +6930,7 @@ var LimeBundleElement = class extends HTMLElement {
6730
6930
  this.bundles = [];
6731
6931
  return;
6732
6932
  }
6733
- const parsed = (0, import_core5.parseMetaobjectBundle)(
6933
+ const parsed = (0, import_core6.parseMetaobjectBundle)(
6734
6934
  data.metaobject.id,
6735
6935
  data.metaobject.fields
6736
6936
  );
@@ -6738,7 +6938,7 @@ var LimeBundleElement = class extends HTMLElement {
6738
6938
  }
6739
6939
  async fetchProductBundles(client, signal, productHandle) {
6740
6940
  const data = await client.query(
6741
- this.wrapQueryForContext(import_core5.BUNDLES_FOR_PRODUCT_QUERY),
6941
+ this.wrapQueryForContext(import_core6.BUNDLES_FOR_PRODUCT_QUERY),
6742
6942
  { handle: productHandle },
6743
6943
  { signal }
6744
6944
  );
@@ -6749,10 +6949,10 @@ var LimeBundleElement = class extends HTMLElement {
6749
6949
  const refs = data.product.metafield?.references?.nodes ?? [];
6750
6950
  const bundles = [];
6751
6951
  for (const ref of refs) {
6752
- const parsed = (0, import_core5.parseMetaobjectBundle)(ref.id, ref.fields);
6952
+ const parsed = (0, import_core6.parseMetaobjectBundle)(ref.id, ref.fields);
6753
6953
  if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
6754
6954
  }
6755
- this.bundles = (0, import_core5.resolveAbTests)(bundles, (0, import_core5.getVisitorId)());
6955
+ this.bundles = (0, import_core6.resolveAbTests)(bundles, (0, import_core6.getVisitorId)());
6756
6956
  }
6757
6957
  /**
6758
6958
  * Dispatch add-to-cart with a cancelable event, then — unless a listener
@@ -6782,33 +6982,59 @@ var LimeBundleElement = class extends HTMLElement {
6782
6982
  */
6783
6983
  async defaultAddToCart(lines) {
6784
6984
  if (typeof window === "undefined") return;
6785
- const client = (0, import_core5.createStorefrontClient)({
6985
+ const client = (0, import_core6.createStorefrontClient)({
6786
6986
  shopDomain: this.shopDomain,
6787
6987
  accessToken: this.storefrontToken
6788
6988
  });
6789
6989
  const storage = window.localStorage;
6790
6990
  const key = cartStorageKey(this.shopDomain);
6791
6991
  const existingCartId = storage?.getItem(key) ?? null;
6992
+ const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
6993
+ const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
6994
+ const fail = (message) => {
6995
+ this.dispatchEvent(
6996
+ new CustomEvent("lime-bundle:error", {
6997
+ detail: { message, code: "CART_ERROR" },
6998
+ bubbles: true,
6999
+ composed: true
7000
+ })
7001
+ );
7002
+ };
6792
7003
  try {
6793
7004
  let checkoutUrl = null;
6794
7005
  if (existingCartId) {
6795
7006
  const res = await client.query(
6796
- import_core5.CART_LINES_ADD_MUTATION,
7007
+ import_core6.CART_LINES_ADD_MUTATION,
6797
7008
  { cartId: existingCartId, lines }
6798
7009
  );
6799
7010
  const payload = res.cartLinesAdd;
6800
7011
  if (payload?.userErrors?.length) {
7012
+ if (!isStaleCartError(payload.userErrors)) {
7013
+ fail(payload.userErrors[0].message);
7014
+ return;
7015
+ }
6801
7016
  storage?.removeItem(key);
7017
+ } else if (stockCapped(payload?.warnings)) {
7018
+ fail("Some items in this bundle are out of stock.");
7019
+ return;
6802
7020
  } else if (payload?.cart) {
6803
7021
  checkoutUrl = payload.cart.checkoutUrl;
6804
7022
  }
6805
7023
  }
6806
7024
  if (!checkoutUrl) {
6807
7025
  const res = await client.query(
6808
- import_core5.CART_CREATE_MUTATION,
7026
+ import_core6.CART_CREATE_MUTATION,
6809
7027
  { input: { lines } }
6810
7028
  );
6811
7029
  const payload = res.cartCreate;
7030
+ if (payload?.userErrors?.length) {
7031
+ fail(payload.userErrors[0].message);
7032
+ return;
7033
+ }
7034
+ if (stockCapped(payload?.warnings)) {
7035
+ fail("Some items in this bundle are out of stock.");
7036
+ return;
7037
+ }
6812
7038
  if (payload?.cart) {
6813
7039
  storage?.setItem(key, payload.cart.id);
6814
7040
  checkoutUrl = payload.cart.checkoutUrl;
@@ -6817,13 +7043,7 @@ var LimeBundleElement = class extends HTMLElement {
6817
7043
  if (checkoutUrl) {
6818
7044
  window.location.assign(checkoutUrl);
6819
7045
  } else {
6820
- this.dispatchEvent(
6821
- new CustomEvent("lime-bundle:error", {
6822
- detail: { message: "Cart creation failed", code: "CART_ERROR" },
6823
- bubbles: true,
6824
- composed: true
6825
- })
6826
- );
7046
+ fail("Cart creation failed");
6827
7047
  }
6828
7048
  } catch (err) {
6829
7049
  this.dispatchEvent(
@@ -6851,7 +7071,7 @@ var LimeBundleElement = class extends HTMLElement {
6851
7071
  const price = variant ? parseFloat(variant.price.amount) : 0;
6852
7072
  return sum + price * line.quantity;
6853
7073
  }, 0);
6854
- (0, import_core5.reportAddToCart)(
7074
+ (0, import_core6.reportAddToCart)(
6855
7075
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
6856
7076
  {
6857
7077
  bundleGid: bundle.id,
@@ -6890,49 +7110,32 @@ var LimeBundleElement = class extends HTMLElement {
6890
7110
  container.setAttribute("aria-label", bundle.title);
6891
7111
  container.setAttribute("data-bundle-type", bundle.bundleType);
6892
7112
  container.setAttribute("data-bundle-gid", bundle.id);
6893
- (0, import_core6.applyWidgetConfigVars)(container, bundle.widgetConfig);
7113
+ if (bundle.endsAt) container.setAttribute("data-ends-at", bundle.endsAt);
7114
+ (0, import_core7.applyWidgetConfigVars)(container, bundle.widgetConfig);
6894
7115
  this.renderCleanups.push(trackInputMode(container));
6895
7116
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
6896
7117
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
6897
7118
  switch (bundle.bundleType) {
6898
7119
  case "fixed":
6899
- renderFixedBundle(
6900
- container,
6901
- bundle,
6902
- this.outOfStockBehavior,
6903
- dispatch,
6904
- registerCleanup
6905
- );
7120
+ renderFixedBundle(container, bundle, dispatch, registerCleanup);
6906
7121
  break;
6907
7122
  case "mix_match":
6908
7123
  renderMixMatchBundle(
6909
7124
  container,
6910
7125
  bundle,
6911
- this.outOfStockBehavior,
6912
7126
  dispatch,
6913
- registerCleanup
7127
+ registerCleanup,
7128
+ this.currentProductHandle
6914
7129
  );
6915
7130
  break;
6916
7131
  case "volume":
6917
- renderVolumeBundle(container, bundle, this.outOfStockBehavior, dispatch);
7132
+ renderVolumeBundle(container, bundle, dispatch);
6918
7133
  break;
6919
7134
  case "bogo":
6920
- renderBogoBundle(
6921
- container,
6922
- bundle,
6923
- this.outOfStockBehavior,
6924
- dispatch,
6925
- registerCleanup
6926
- );
7135
+ renderBogoBundle(container, bundle, dispatch, registerCleanup);
6927
7136
  break;
6928
7137
  case "multi_step":
6929
- renderMultiStepBundle(
6930
- container,
6931
- bundle,
6932
- this.outOfStockBehavior,
6933
- dispatch,
6934
- registerCleanup
6935
- );
7138
+ renderMultiStepBundle(container, bundle, dispatch, registerCleanup);
6936
7139
  break;
6937
7140
  }
6938
7141
  this.shadow.appendChild(container);
@@ -6956,8 +7159,8 @@ var LimeBundleElement = class extends HTMLElement {
6956
7159
  }
6957
7160
  setupImpressionFor(bundle, element) {
6958
7161
  if (!this.analyticsEnabled || !this.appUrl) return;
6959
- const cleanup = (0, import_core5.observeImpression)(element, () => {
6960
- (0, import_core5.reportImpression)(
7162
+ const cleanup = (0, import_core6.observeImpression)(element, () => {
7163
+ (0, import_core6.reportImpression)(
6961
7164
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
6962
7165
  {
6963
7166
  bundleGid: bundle.id,