@lime-bundles/widget 4.1.1 → 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/README.md CHANGED
@@ -129,7 +129,7 @@ The widget renders its own inline fallback; listen for this event if you want to
129
129
  - **Countdown timer.** When a bundle has `endsAt`, the widget ticks live and hides on expiry.
130
130
  - **Variant dropdowns.** Products with multiple eligible variants render a `<select>`; switching live-updates the row price and bundle total.
131
131
  - **Mix-match picker modal.** Full modal with search, quantity stepper, progress bar, focus trap, and keyboard navigation.
132
- - **Out-of-stock handling.** Honours `widgetConfig.outOfStockBehavior` (`hide` vs `show_greyed_out`); hides the widget when the bundle is unfulfillable.
132
+ - **Out-of-stock handling.** Sold-out products render greyed out with the CTA locked; a bundle that can't be completed doesn't render.
133
133
  - **A/B testing.** Reads the merchant's A/B config, buckets the visitor via a first-party cookie, applies Variant B overrides.
134
134
  - **Analytics.** Impression and add-to-cart events fire regardless of whether you override the cart.
135
135
 
package/dist/index.cjs CHANGED
@@ -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++) {
@@ -1177,12 +1188,11 @@ function bindDropdowns(root) {
1177
1188
  }
1178
1189
 
1179
1190
  // src/renderers/fixed.ts
1180
- function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
1191
+ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
1181
1192
  const wc = bundle.widgetConfig;
1182
1193
  const products = adaptProducts(bundle);
1183
1194
  if (!products.length) return;
1184
1195
  const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));
1185
- if (outOfStockBehavior === "hide" && oosProducts.length > 0) return;
1186
1196
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
1187
1197
  const formatMoney = intlFormatMoney(currency);
1188
1198
  const t = DEFAULT_STRINGS;
@@ -1207,7 +1217,9 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
1207
1217
  });
1208
1218
  shell.products.appendChild(row);
1209
1219
  if (isOos) continue;
1210
- const 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);
1211
1223
  hydrateRowControls(row, product, selected);
1212
1224
  applyVariant(row, selected, product, formatMoney);
1213
1225
  bindVariantSelects(
@@ -1802,6 +1814,17 @@ function unlock() {
1802
1814
  window.scrollTo(0, savedY);
1803
1815
  }
1804
1816
 
1817
+ // ../render/src/picker/sort.ts
1818
+ function sinkAddedRows(list, rowEls, isAdded) {
1819
+ const front = [];
1820
+ const back = [];
1821
+ for (let i = 0; i < rowEls.length; i++) {
1822
+ (isAdded(i) ? back : front).push(rowEls[i]);
1823
+ }
1824
+ for (const el2 of front) list.appendChild(el2);
1825
+ for (const el2 of back) list.appendChild(el2);
1826
+ }
1827
+
1805
1828
  // ../render/src/mix-match/modal.ts
1806
1829
  var ALL_TYPES = "__all__";
1807
1830
  var CLOSE_TRANSITION_MS = 300;
@@ -2026,12 +2049,22 @@ function createPickerModal(deps) {
2026
2049
  syncActiveFilterPill();
2027
2050
  if (searchInput) searchInput.value = "";
2028
2051
  filterProducts(searchInput ? searchInput.value : "");
2052
+ if (modalList) {
2053
+ const selectedProductIds = /* @__PURE__ */ Object.create(null);
2054
+ for (const it of items) selectedProductIds[String(it.productId)] = true;
2055
+ sinkAddedRows(
2056
+ modalList,
2057
+ rows.map((r) => r.el),
2058
+ (i) => selectedProductIds[String(eligibleProducts[i].id)] === true
2059
+ );
2060
+ modalList.scrollTop = 0;
2061
+ }
2029
2062
  overlay.style.display = "";
2030
2063
  void overlay.offsetHeight;
2031
2064
  overlay.classList.add("lb-mix-match__modal-overlay--open");
2032
2065
  lock();
2033
2066
  setTimeout(() => {
2034
- if (searchInput) searchInput.focus();
2067
+ if (closeBtn) closeBtn.focus();
2035
2068
  else modalDialog?.focus();
2036
2069
  }, 50);
2037
2070
  }
@@ -2588,7 +2621,7 @@ function buildPickerModal(opts) {
2588
2621
  }
2589
2622
 
2590
2623
  // src/renderers/mix-match.ts
2591
- function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup, currentProductHandle) {
2624
+ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, currentProductHandle) {
2592
2625
  const wc = bundle.widgetConfig;
2593
2626
  const t = DEFAULT_STRINGS;
2594
2627
  const requiredQty = bundle.minQuantity || 1;
@@ -2611,12 +2644,12 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2611
2644
  compareAtPrice: v.compareAtPrice,
2612
2645
  unitPrice: v.unitPrice,
2613
2646
  image: v.image,
2614
- inventoryQuantity: null
2647
+ inventoryQuantity: v.inventoryQuantity ?? null
2615
2648
  }))
2616
2649
  }));
2617
2650
  if (!eligibleProducts.length) return;
2618
2651
  const poolUnits = eligibleProducts.filter((p) => p.available).length;
2619
- if (outOfStockBehavior === "hide" && poolUnits === 0) return;
2652
+ if (poolUnits === 0) return;
2620
2653
  const formatMoney = intlFormatMoney(
2621
2654
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
2622
2655
  );
@@ -2779,6 +2812,44 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2779
2812
  // src/renderers/volume.ts
2780
2813
  var import_core4 = require("@lime-bundles/core");
2781
2814
 
2815
+ // ../render/src/volume/stock.ts
2816
+ var TIER_OOS_CLASS = "lb-volume__tier--oos";
2817
+ function tierQty(el2) {
2818
+ return parseInt(el2.getAttribute("data-tier-qty") ?? "", 10) || 0;
2819
+ }
2820
+ function tierUnavailable(el2) {
2821
+ return el2.getAttribute("aria-disabled") === "true";
2822
+ }
2823
+ function applyTierStockState(tierEls, cap) {
2824
+ for (let i = 0; i < tierEls.length; i++) {
2825
+ const el2 = tierEls[i];
2826
+ const unavailable = cap !== null && tierQty(el2) > cap;
2827
+ if (unavailable) {
2828
+ el2.classList.add(TIER_OOS_CLASS);
2829
+ el2.setAttribute("aria-disabled", "true");
2830
+ } else {
2831
+ el2.classList.remove(TIER_OOS_CLASS);
2832
+ el2.removeAttribute("aria-disabled");
2833
+ }
2834
+ }
2835
+ }
2836
+ function resolveAvailableTierIndex(tierEls, preferred) {
2837
+ const p = tierEls[preferred];
2838
+ if (p && !tierUnavailable(p)) return preferred;
2839
+ for (let i = 0; i < tierEls.length; i++) {
2840
+ if (!tierUnavailable(tierEls[i])) return i;
2841
+ }
2842
+ return -1;
2843
+ }
2844
+ function stepToAvailableTier(tierEls, from, direction) {
2845
+ let i = from + direction;
2846
+ while (i >= 0 && i < tierEls.length) {
2847
+ if (!tierUnavailable(tierEls[i])) return i;
2848
+ i += direction;
2849
+ }
2850
+ return from;
2851
+ }
2852
+
2782
2853
  // ../render/src/volume/pricing.ts
2783
2854
  function calcTierPrice(basePrice, discountType, tierEl) {
2784
2855
  if (discountType === "fixed_amount") {
@@ -2868,15 +2939,16 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
2868
2939
  }
2869
2940
 
2870
2941
  // src/renderers/volume.ts
2871
- function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart) {
2942
+ function renderVolumeBundle(container, bundle, onAddToCart) {
2872
2943
  const wc = bundle.widgetConfig;
2873
2944
  const product = bundle.products[0];
2874
2945
  if (!product) return;
2875
2946
  const variants = product.variants.nodes;
2876
- 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];
2877
2951
  if (!pricingVariant) return;
2878
- const anyAvailable = variants.some((v) => v.availableForSale);
2879
- if (outOfStockBehavior === "hide" && !anyAvailable) return;
2880
2952
  const basePrice = toCents(pricingVariant.price.amount);
2881
2953
  const discountType = bundle.discountConfig.discountType;
2882
2954
  const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
@@ -2927,28 +2999,31 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
2927
2999
  shell.products.replaceWith(group);
2928
3000
  const tierEls = group.querySelectorAll("[data-tier-index]");
2929
3001
  updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
2930
- let selectedIndex = defaultIdx;
3002
+ applyTierStockState(tierEls, (0, import_core4.stockCapForVariant)(pricingVariant));
3003
+ let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);
3004
+ const noTierAvailable = selectedIndex === -1;
3005
+ if (noTierAvailable) selectedIndex = defaultIdx;
2931
3006
  const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
2932
3007
  group.addEventListener("click", (e) => {
2933
3008
  const el2 = e.target.closest("[data-tier-index]");
2934
- if (!el2) return;
3009
+ if (!el2 || tierUnavailable(el2)) return;
2935
3010
  selectedIndex = parseInt(el2.getAttribute("data-tier-index") ?? "", 10) || 0;
2936
3011
  apply();
2937
3012
  });
2938
3013
  group.addEventListener("keydown", (e) => {
2939
3014
  if (e.key === "ArrowDown" || e.key === "ArrowRight") {
2940
3015
  e.preventDefault();
2941
- selectedIndex = Math.min(selectedIndex + 1, tierEls.length - 1);
3016
+ selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);
2942
3017
  apply();
2943
3018
  tierEls[selectedIndex].focus();
2944
3019
  } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
2945
3020
  e.preventDefault();
2946
- selectedIndex = Math.max(selectedIndex - 1, 0);
3021
+ selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);
2947
3022
  apply();
2948
3023
  tierEls[selectedIndex].focus();
2949
3024
  }
2950
3025
  });
2951
- if (!anyAvailable) {
3026
+ if (noTierAvailable) {
2952
3027
  shell.cta.disabled = true;
2953
3028
  const label = shell.cta.querySelector("[data-cta-label]");
2954
3029
  if (label) label.textContent = t.outOfStock || "Out of stock";
@@ -3083,7 +3158,7 @@ function buildBogoPlus() {
3083
3158
  plus.innerHTML = '<svg width="12" height="12" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="9" y1="3" x2="9" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="9" x2="15" y2="9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
3084
3159
  return plus;
3085
3160
  }
3086
- function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
3161
+ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3087
3162
  const wc = bundle.widgetConfig;
3088
3163
  const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
3089
3164
  const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);
@@ -3112,7 +3187,6 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3112
3187
  }
3113
3188
  ];
3114
3189
  const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;
3115
- if (outOfStockBehavior === "hide" && oosCount > 0) return;
3116
3190
  const formatMoney = intlFormatMoney(
3117
3191
  buyProduct.priceRange.minVariantPrice.currencyCode
3118
3192
  );
@@ -3545,6 +3619,14 @@ function createWizard(deps) {
3545
3619
  rows.push(row);
3546
3620
  modalList.appendChild(row.el);
3547
3621
  }
3622
+ const stepPicks = selections[forStep] || [];
3623
+ const selectedProductIds = /* @__PURE__ */ Object.create(null);
3624
+ for (const it of stepPicks) selectedProductIds[String(it.productId)] = true;
3625
+ sinkAddedRows(
3626
+ modalList,
3627
+ rows.map((r) => r.el),
3628
+ (i) => selectedProductIds[String(pool[i].id)] === true
3629
+ );
3548
3630
  deps.dropdown?.bindAll(modalList);
3549
3631
  }
3550
3632
  function onListClick(e) {
@@ -3707,6 +3789,7 @@ function createWizard(deps) {
3707
3789
  syncActiveFilterPill();
3708
3790
  if (searchInput) searchInput.value = "";
3709
3791
  filterProducts("");
3792
+ if (modalList) modalList.scrollTop = 0;
3710
3793
  buildStepSegments(stepIndex);
3711
3794
  refresh();
3712
3795
  if (modalLive) {
@@ -3730,7 +3813,7 @@ function createWizard(deps) {
3730
3813
  overlay.classList.add("lb-mix-match__modal-overlay--open");
3731
3814
  lock();
3732
3815
  setTimeout(() => {
3733
- if (searchInput) searchInput.focus();
3816
+ if (closeBtn) closeBtn.focus();
3734
3817
  else modalDialog?.focus();
3735
3818
  }, 50);
3736
3819
  }
@@ -3798,7 +3881,7 @@ function createWizard(deps) {
3798
3881
  }
3799
3882
 
3800
3883
  // src/renderers/multi-step.ts
3801
- function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
3884
+ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
3802
3885
  const wc = bundle.widgetConfig;
3803
3886
  const t = DEFAULT_STRINGS;
3804
3887
  const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
@@ -3831,13 +3914,13 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3831
3914
  compareAtPrice: v.compareAtPrice,
3832
3915
  unitPrice: v.unitPrice,
3833
3916
  image: v.image,
3834
- inventoryQuantity: null
3917
+ inventoryQuantity: v.inventoryQuantity ?? null
3835
3918
  }))
3836
3919
  };
3837
3920
  })
3838
3921
  );
3839
- const anyPickable = pools.some((pool) => pool.some((p) => p.available));
3840
- if (outOfStockBehavior === "hide" && !anyPickable) return;
3922
+ const everyStepPickable = pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));
3923
+ if (!everyStepPickable) return;
3841
3924
  const formatMoney = intlFormatMoney(
3842
3925
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
3843
3926
  );
@@ -5649,7 +5732,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5649
5732
  transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
5650
5733
  }
5651
5734
 
5652
- .lb-mix-match__modal-add:hover {
5735
+ .lb-mix-match__modal-add:hover:not(:disabled) {
5653
5736
  opacity: 0.9;
5654
5737
  }
5655
5738
 
@@ -5764,8 +5847,15 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5764
5847
  color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);
5765
5848
  }
5766
5849
 
5767
- /* "Done" reuses the picker Add-button styling so it matches the Add buttons. */
5850
+ /* "Done" reuses the picker Add-button styling so it matches the Add buttons.
5851
+ The flex centring + min-height pin the footer-button silhouette: the wizard
5852
+ Back (bundle-multi-step.css) copies these metrics so the pair stays
5853
+ equal-height whatever border widths the merchant's picker tokens set. */
5768
5854
  .lb-mix-match__modal-done {
5855
+ display: inline-flex;
5856
+ align-items: center;
5857
+ justify-content: center;
5858
+ min-height: 40px;
5769
5859
  padding: 10px 24px;
5770
5860
  transition: opacity 0.25s ease;
5771
5861
  background: var(--lb-picker-add-bg);
@@ -5779,7 +5869,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5779
5869
  cursor: pointer;
5780
5870
  }
5781
5871
 
5782
- .lb-mix-match__modal-done:hover {
5872
+ .lb-mix-match__modal-done:hover:not(:disabled) {
5783
5873
  opacity: 0.9;
5784
5874
  }
5785
5875
 
@@ -5789,6 +5879,16 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5789
5879
  box-shadow: none;
5790
5880
  }
5791
5881
 
5882
+ /* Disabled Next/Done (wizard step minimum not met) \u2014 same washed-out
5883
+ treatment as a disabled row Add, so an unfinished step reads at a
5884
+ glance. Hover is gated off above; the solid button returns the moment
5885
+ the requirement is met. */
5886
+ .lb-mix-match__modal-done:disabled {
5887
+ background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));
5888
+ color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);
5889
+ cursor: not-allowed;
5890
+ }
5891
+
5792
5892
  /* Hidden utility for search filtering */
5793
5893
  .lb-hidden {
5794
5894
  display: none !important;
@@ -6045,6 +6145,13 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
6045
6145
  white-space: nowrap;
6046
6146
  font-variant-numeric: tabular-nums;
6047
6147
  }
6148
+
6149
+ /* Tier the selected variant can't cover \u2014 same greyed treatment as an
6150
+ out-of-stock product row, and not selectable. */
6151
+ .lb-volume__tier--oos {
6152
+ opacity: 0.5;
6153
+ cursor: not-allowed;
6154
+ }
6048
6155
  `;
6049
6156
  var BUNDLE_BOGO_CSS = `/* Lime Bundles \u2014 BOGO (Buy X Get Y) bundle styles */
6050
6157
 
@@ -6213,14 +6320,23 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6213
6320
  justify-self: end;
6214
6321
  }
6215
6322
 
6216
- /* Back \u2014 secondary treatment beside the primary Next/Done (which reuse
6217
- .lb-mix-match__modal-done). */
6323
+ /* Back \u2014 secondary (outlined) treatment beside the primary Next/Done
6324
+ (which reuse .lb-mix-match__modal-done). Same silhouette as Next \u2014
6325
+ metrics copied from the modal-done recipe (padding, type, radius,
6326
+ min-height) \u2014 and picker tokens only: the modal is portaled out of the
6327
+ widget, so the widget vars (--lb-text, --lb-border-*) it previously
6328
+ used don't reliably resolve here and break palette isolation. */
6218
6329
  .lb-multi-step__modal-back {
6219
- padding: 10px 16px;
6330
+ display: inline-flex;
6331
+ align-items: center;
6332
+ justify-content: center;
6333
+ min-height: 40px;
6334
+ padding: 10px 24px;
6220
6335
  background: none;
6221
- border: var(--lb-border-width) solid var(--lb-border-color);
6222
- border-radius: var(--lb-radius-sm);
6223
- 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);
6224
6340
  font-family: inherit;
6225
6341
  font-size: 14px;
6226
6342
  font-weight: 600;
@@ -6229,7 +6345,7 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6229
6345
  }
6230
6346
 
6231
6347
  .lb-multi-step__modal-back:hover {
6232
- background: color-mix(in srgb, var(--lb-text) 5%, transparent);
6348
+ background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);
6233
6349
  }
6234
6350
 
6235
6351
  .lb-multi-step__modal-back:focus-visible {
@@ -6659,12 +6775,6 @@ var LimeBundleElement = class extends HTMLElement {
6659
6775
  * `.lb-bundle-widget { ... }` reach the widget's DOM.
6660
6776
  */
6661
6777
  shopCustomCss = null;
6662
- /**
6663
- * Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
6664
- * before the first render. Defaults to `show_greyed_out` so a failed or
6665
- * missing fetch degrades to "bundles still render".
6666
- */
6667
- outOfStockBehavior = import_core6.DEFAULT_OUT_OF_STOCK_BEHAVIOR;
6668
6778
  constructor() {
6669
6779
  super();
6670
6780
  this.shadow = this.attachShadow({ mode: "open" });
@@ -6800,9 +6910,6 @@ var LimeBundleElement = class extends HTMLElement {
6800
6910
  const sanitized = (0, import_core6.sanitizeCustomCss)(customCss);
6801
6911
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
6802
6912
  }
6803
- this.outOfStockBehavior = (0, import_core6.parseOutOfStockBehavior)(
6804
- shopSettings?.shop?.outOfStockBehavior?.value
6805
- );
6806
6913
  this.renderBundles();
6807
6914
  } catch (err) {
6808
6915
  if (controller.signal.aborted) return;
@@ -6882,6 +6989,17 @@ var LimeBundleElement = class extends HTMLElement {
6882
6989
  const storage = window.localStorage;
6883
6990
  const key = cartStorageKey(this.shopDomain);
6884
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
+ };
6885
7003
  try {
6886
7004
  let checkoutUrl = null;
6887
7005
  if (existingCartId) {
@@ -6891,7 +7009,14 @@ var LimeBundleElement = class extends HTMLElement {
6891
7009
  );
6892
7010
  const payload = res.cartLinesAdd;
6893
7011
  if (payload?.userErrors?.length) {
7012
+ if (!isStaleCartError(payload.userErrors)) {
7013
+ fail(payload.userErrors[0].message);
7014
+ return;
7015
+ }
6894
7016
  storage?.removeItem(key);
7017
+ } else if (stockCapped(payload?.warnings)) {
7018
+ fail("Some items in this bundle are out of stock.");
7019
+ return;
6895
7020
  } else if (payload?.cart) {
6896
7021
  checkoutUrl = payload.cart.checkoutUrl;
6897
7022
  }
@@ -6902,6 +7027,14 @@ var LimeBundleElement = class extends HTMLElement {
6902
7027
  { input: { lines } }
6903
7028
  );
6904
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
+ }
6905
7038
  if (payload?.cart) {
6906
7039
  storage?.setItem(key, payload.cart.id);
6907
7040
  checkoutUrl = payload.cart.checkoutUrl;
@@ -6910,13 +7043,7 @@ var LimeBundleElement = class extends HTMLElement {
6910
7043
  if (checkoutUrl) {
6911
7044
  window.location.assign(checkoutUrl);
6912
7045
  } else {
6913
- this.dispatchEvent(
6914
- new CustomEvent("lime-bundle:error", {
6915
- detail: { message: "Cart creation failed", code: "CART_ERROR" },
6916
- bubbles: true,
6917
- composed: true
6918
- })
6919
- );
7046
+ fail("Cart creation failed");
6920
7047
  }
6921
7048
  } catch (err) {
6922
7049
  this.dispatchEvent(
@@ -6990,44 +7117,25 @@ var LimeBundleElement = class extends HTMLElement {
6990
7117
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
6991
7118
  switch (bundle.bundleType) {
6992
7119
  case "fixed":
6993
- renderFixedBundle(
6994
- container,
6995
- bundle,
6996
- this.outOfStockBehavior,
6997
- dispatch,
6998
- registerCleanup
6999
- );
7120
+ renderFixedBundle(container, bundle, dispatch, registerCleanup);
7000
7121
  break;
7001
7122
  case "mix_match":
7002
7123
  renderMixMatchBundle(
7003
7124
  container,
7004
7125
  bundle,
7005
- this.outOfStockBehavior,
7006
7126
  dispatch,
7007
7127
  registerCleanup,
7008
7128
  this.currentProductHandle
7009
7129
  );
7010
7130
  break;
7011
7131
  case "volume":
7012
- renderVolumeBundle(container, bundle, this.outOfStockBehavior, dispatch);
7132
+ renderVolumeBundle(container, bundle, dispatch);
7013
7133
  break;
7014
7134
  case "bogo":
7015
- renderBogoBundle(
7016
- container,
7017
- bundle,
7018
- this.outOfStockBehavior,
7019
- dispatch,
7020
- registerCleanup
7021
- );
7135
+ renderBogoBundle(container, bundle, dispatch, registerCleanup);
7022
7136
  break;
7023
7137
  case "multi_step":
7024
- renderMultiStepBundle(
7025
- container,
7026
- bundle,
7027
- this.outOfStockBehavior,
7028
- dispatch,
7029
- registerCleanup
7030
- );
7138
+ renderMultiStepBundle(container, bundle, dispatch, registerCleanup);
7031
7139
  break;
7032
7140
  }
7033
7141
  this.shadow.appendChild(container);
package/dist/index.d.cts CHANGED
@@ -82,12 +82,6 @@ declare class LimeBundleElement extends HTMLElement {
82
82
  * `.lb-bundle-widget { ... }` reach the widget's DOM.
83
83
  */
84
84
  private shopCustomCss;
85
- /**
86
- * Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
87
- * before the first render. Defaults to `show_greyed_out` so a failed or
88
- * missing fetch degrades to "bundles still render".
89
- */
90
- private outOfStockBehavior;
91
85
  constructor();
92
86
  connectedCallback(): void;
93
87
  disconnectedCallback(): void;
package/dist/index.d.ts CHANGED
@@ -82,12 +82,6 @@ declare class LimeBundleElement extends HTMLElement {
82
82
  * `.lb-bundle-widget { ... }` reach the widget's DOM.
83
83
  */
84
84
  private shopCustomCss;
85
- /**
86
- * Shop-wide out-of-stock behaviour, resolved from the `$app` shop metafield
87
- * before the first render. Defaults to `show_greyed_out` so a failed or
88
- * missing fetch degrades to "bundles still render".
89
- */
90
- private outOfStockBehavior;
91
85
  constructor();
92
86
  connectedCallback(): void;
93
87
  disconnectedCallback(): void;