@lime-bundles/react 2.4.0 → 3.0.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
@@ -349,6 +349,20 @@ var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = imp
349
349
  var ITEM_HEIGHT_PX = 32;
350
350
  var LIST_PAD_Y = 8;
351
351
  var MAX_VISIBLE_ITEMS = 8;
352
+ function findScrollableAncestor(el) {
353
+ const win = el.ownerDocument?.defaultView;
354
+ if (!win) return null;
355
+ let cur = el.parentElement;
356
+ while (cur && cur !== el.ownerDocument.body) {
357
+ const style = win.getComputedStyle(cur);
358
+ const overflowY = style.overflowY;
359
+ if (overflowY === "auto" || overflowY === "scroll" || overflowY === "hidden") {
360
+ return cur;
361
+ }
362
+ cur = cur.parentElement;
363
+ }
364
+ return null;
365
+ }
352
366
  function VariantDropdown({
353
367
  options,
354
368
  value,
@@ -398,6 +412,11 @@ function VariantDropdown({
398
412
  if (rect.width === 0) return;
399
413
  const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
400
414
  const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
415
+ const scrollable = findScrollableAncestor(trigger);
416
+ const clip = scrollable ? (() => {
417
+ const r = scrollable.getBoundingClientRect();
418
+ return { top: r.top, bottom: r.bottom };
419
+ })() : void 0;
401
420
  setPosition(
402
421
  computePosition({
403
422
  trigger: {
@@ -407,7 +426,8 @@ function VariantDropdown({
407
426
  width: rect.width
408
427
  },
409
428
  viewportHeight: window.innerHeight,
410
- desiredHeight
429
+ desiredHeight,
430
+ clip
411
431
  })
412
432
  );
413
433
  }, [isOpen, options.length]);
@@ -641,8 +661,14 @@ function FixedBundle(props) {
641
661
  }, [result, onError]);
642
662
  const handleAddToCart = (0, import_react6.useCallback)(async () => {
643
663
  if (!bundle) return;
644
- const bundleLines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
645
- const variant = selectedVariants[p.id] ?? p.variants.nodes.find((v) => v.availableForSale);
664
+ const bundleLines = bundle.products.filter(
665
+ (p) => p.variants.nodes.some(
666
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, p.id, v.id))
667
+ )
668
+ ).map((p) => {
669
+ const variant = selectedVariants[p.id] ?? p.variants.nodes.find(
670
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, p.id, v.id))
671
+ );
646
672
  return {
647
673
  productId: p.id,
648
674
  variant,
@@ -712,6 +738,7 @@ function FixedBundle(props) {
712
738
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
713
739
  FixedProductRow,
714
740
  {
741
+ bundle,
715
742
  product,
716
743
  currency,
717
744
  onVariantChange: handleVariantChange
@@ -733,7 +760,7 @@ function FixedBundle(props) {
733
760
  }
734
761
  );
735
762
  }
736
- function FixedProductRow({ product, currency, onVariantChange }) {
763
+ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
737
764
  const variants = product.variants.nodes;
738
765
  const optionNames = (0, import_react6.useMemo)(() => {
739
766
  const first = variants[0];
@@ -747,7 +774,9 @@ function FixedProductRow({ product, currency, onVariantChange }) {
747
774
  (0, import_react6.useEffect)(() => {
748
775
  onVariantChange(product.id, selectedVariant ?? null);
749
776
  }, [selectedVariant, product.id, onVariantChange]);
750
- const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
777
+ const displayVariant = selectedVariant ?? variants.find(
778
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, product.id, v.id))
779
+ ) ?? variants[0];
751
780
  const priceText = displayVariant ? (0, import_core8.formatMoney)(displayVariant.price.amount, currency) : (0, import_core8.formatMoney)(product.priceRange.minVariantPrice.amount, currency);
752
781
  const unitPriceText = displayVariant ? (0, import_core8.formatUnitPrice)(
753
782
  displayVariant.unitPrice,
@@ -755,6 +784,12 @@ function FixedProductRow({ product, currency, onVariantChange }) {
755
784
  currency
756
785
  ) : null;
757
786
  const thumbImage = displayVariant?.image ?? product.featuredImage;
787
+ const showLowStock = !!displayVariant && (0, import_core8.shouldShowLowStockBadge)(
788
+ displayVariant,
789
+ (0, import_core8.resolveBundleQty)(bundle, product.id, displayVariant.id),
790
+ bundle.widgetConfig.lowStockThreshold,
791
+ bundle.widgetConfig.showLowStockBadge
792
+ );
758
793
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
759
794
  thumbImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
760
795
  "img",
@@ -769,6 +804,11 @@ function FixedProductRow({ product, currency, onVariantChange }) {
769
804
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
770
805
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: priceText }),
771
806
  unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
807
+ showLowStock && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
808
+ "Only ",
809
+ displayVariant.quantityAvailable,
810
+ " left"
811
+ ] }),
772
812
  showPicker && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
773
813
  const dropdownOptions = optionsFor(optionIndex).map((o) => ({
774
814
  value: o.value,
@@ -794,6 +834,9 @@ function FixedProductRow({ product, currency, onVariantChange }) {
794
834
  var import_react7 = require("react");
795
835
  var import_core9 = require("@lime-bundles/core");
796
836
  var import_jsx_runtime3 = require("react/jsx-runtime");
837
+ function ruleFor(bundle, productId) {
838
+ return bundle.productRules[productId] ?? import_core9.DEFAULT_PRODUCT_RULE;
839
+ }
797
840
  function MixMatchBundle(props) {
798
841
  const {
799
842
  shopDomain,
@@ -837,56 +880,92 @@ function MixMatchBundle(props) {
837
880
  );
838
881
  }
839
882
  }, [result, onError]);
840
- const totalQuantity = Array.from(selections.values()).reduce(
841
- (sum, s) => sum + s.quantity,
842
- 0
883
+ const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
884
+ const distinctSelectedProducts = (0, import_react7.useMemo)(() => {
885
+ const ids = /* @__PURE__ */ new Set();
886
+ for (const sel of selections.values()) {
887
+ if (sel.quantity > 0) ids.add(sel.productId);
888
+ }
889
+ return ids;
890
+ }, [selections]);
891
+ const totalQuantity = (0, import_react7.useMemo)(
892
+ () => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
893
+ [selections]
843
894
  );
844
- const validation = bundle ? (0, import_core9.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
845
- const toggleProduct = (0, import_react7.useCallback)(
895
+ const requiredPicks = bundle?.minQuantity ?? 0;
896
+ const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
897
+ const valid = !!bundle && meetsMinPicks;
898
+ const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
899
+ const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
900
+ const selectVariant = (0, import_react7.useCallback)(
846
901
  (productId, variant) => {
902
+ if (!bundle) return;
903
+ const rule = ruleFor(bundle, productId);
904
+ const maxAddable = (0, import_core9.maxAddableQuantity)(variant, rule.max, 0);
905
+ if (maxAddable < rule.min) return;
847
906
  setSelections((prev) => {
848
907
  const next = new Map(prev);
849
- const key = `${productId}:${variant.id}`;
850
- if (next.has(key)) {
851
- next.delete(key);
852
- } else {
853
- const configuredQty = bundle ? (0, import_core9.resolveBundleQty)(bundle, productId, variant.id) : 1;
854
- next.set(key, {
855
- productId,
856
- variantId: variant.id,
857
- quantity: configuredQty
858
- });
859
- }
908
+ next.set(`${productId}:${variant.id}`, {
909
+ productId,
910
+ variantId: variant.id,
911
+ quantity: rule.min
912
+ });
860
913
  return next;
861
914
  });
862
915
  },
863
916
  [bundle]
864
917
  );
865
- const updateQuantity = (0, import_react7.useCallback)(
866
- (productId, variantId, quantity) => {
918
+ const deselect = (0, import_react7.useCallback)((productId, variantId) => {
919
+ setSelections((prev) => {
920
+ if (!prev.has(`${productId}:${variantId}`)) return prev;
921
+ const next = new Map(prev);
922
+ next.delete(`${productId}:${variantId}`);
923
+ return next;
924
+ });
925
+ }, []);
926
+ const setQuantity = (0, import_react7.useCallback)(
927
+ (productId, variant, quantity) => {
928
+ if (!bundle) return;
929
+ const rule = ruleFor(bundle, productId);
930
+ const maxAddable = (0, import_core9.maxAddableQuantity)(variant, rule.max, 0);
931
+ const key = `${productId}:${variant.id}`;
867
932
  setSelections((prev) => {
868
933
  const next = new Map(prev);
869
- const key = `${productId}:${variantId}`;
870
- if (quantity <= 0) {
934
+ if (quantity < rule.min || maxAddable < rule.min) {
871
935
  next.delete(key);
872
- } else {
873
- next.set(key, { productId, variantId, quantity });
936
+ return next;
874
937
  }
938
+ const clamped = Math.min(quantity, maxAddable);
939
+ next.set(key, {
940
+ productId,
941
+ variantId: variant.id,
942
+ quantity: clamped
943
+ });
875
944
  return next;
876
945
  });
877
946
  },
878
- []
947
+ [bundle]
879
948
  );
880
949
  const handleAddToCart = (0, import_react7.useCallback)(async () => {
881
- if (!bundle || !validation.valid) return;
882
- const lines = Array.from(selections.values()).map((s) => ({
883
- merchandiseId: s.variantId,
884
- quantity: s.quantity,
885
- attributes: [
886
- { key: "_lime_bundle_gid", value: bundle.id },
887
- { key: "_lime_bundle_type", value: bundle.bundleType }
888
- ]
889
- }));
950
+ if (!bundle || !valid) return;
951
+ const byVariant = /* @__PURE__ */ new Map();
952
+ for (const sel of selections.values()) {
953
+ if (sel.quantity <= 0) continue;
954
+ byVariant.set(
955
+ sel.variantId,
956
+ (byVariant.get(sel.variantId) ?? 0) + sel.quantity
957
+ );
958
+ }
959
+ const lines = Array.from(byVariant.entries()).map(
960
+ ([variantId, quantity]) => ({
961
+ merchandiseId: variantId,
962
+ quantity,
963
+ attributes: [
964
+ { key: "_lime_bundle_gid", value: bundle.id },
965
+ { key: "_lime_bundle_type", value: bundle.bundleType }
966
+ ]
967
+ })
968
+ );
890
969
  setAddingToCart(true);
891
970
  setCartError(null);
892
971
  try {
@@ -912,15 +991,7 @@ function MixMatchBundle(props) {
912
991
  } finally {
913
992
  setAddingToCart(false);
914
993
  }
915
- }, [
916
- bundle,
917
- selections,
918
- validation.valid,
919
- onAddToCart,
920
- onError,
921
- trackAddToCart,
922
- totalQuantity
923
- ]);
994
+ }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
924
995
  if (result.status === "loading") {
925
996
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
926
997
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
@@ -943,26 +1014,29 @@ function MixMatchBundle(props) {
943
1014
  children: [
944
1015
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
945
1016
  bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
946
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
1017
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
947
1018
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
948
1019
  MixMatchProductRow,
949
1020
  {
1021
+ bundle,
950
1022
  product,
951
1023
  currency,
1024
+ showStepper,
952
1025
  selections,
953
- onToggle: toggleProduct,
954
- onUpdateQuantity: updateQuantity
1026
+ onSelect: selectVariant,
1027
+ onDeselect: deselect,
1028
+ onSetQuantity: setQuantity
955
1029
  },
956
1030
  product.id
957
1031
  )) }),
958
- validation.message && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
1032
+ validationMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
959
1033
  cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
960
1034
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
961
1035
  "button",
962
1036
  {
963
1037
  className: "lb-bundle__cta",
964
1038
  onClick: handleAddToCart,
965
- disabled: addingToCart || !validation.valid,
1039
+ disabled: addingToCart || !valid,
966
1040
  "aria-busy": addingToCart,
967
1041
  children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
968
1042
  }
@@ -972,11 +1046,14 @@ function MixMatchBundle(props) {
972
1046
  );
973
1047
  }
974
1048
  function MixMatchProductRow({
1049
+ bundle,
975
1050
  product,
976
1051
  currency,
1052
+ showStepper,
977
1053
  selections,
978
- onToggle,
979
- onUpdateQuantity
1054
+ onSelect,
1055
+ onDeselect,
1056
+ onSetQuantity
980
1057
  }) {
981
1058
  const variants = product.variants.nodes;
982
1059
  const optionNames = (0, import_react7.useMemo)(() => {
@@ -984,9 +1061,12 @@ function MixMatchProductRow({
984
1061
  return first ? first.selectedOptions.map((o) => o.name) : [];
985
1062
  }, [variants]);
986
1063
  const showPicker = variants.length > 1 && optionNames.length > 0;
1064
+ const rule = ruleFor(bundle, product.id);
987
1065
  const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
988
- const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
1066
+ const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core9.isVariantFulfillable)(v, rule.min)) ?? variants[0] ?? null;
989
1067
  if (!displayVariant) return null;
1068
+ const fulfillable = (0, import_core9.isVariantFulfillable)(displayVariant, rule.min);
1069
+ const maxAddable = (0, import_core9.maxAddableQuantity)(displayVariant, rule.max, 0);
990
1070
  const key = `${product.id}:${displayVariant.id}`;
991
1071
  const selected = selections.get(key);
992
1072
  const thumbImage = displayVariant.image ?? product.featuredImage;
@@ -1013,6 +1093,16 @@ function MixMatchProductRow({
1013
1093
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1014
1094
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core9.formatMoney)(displayVariant.price.amount, currency) }),
1015
1095
  unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1096
+ (0, import_core9.shouldShowLowStockBadge)(
1097
+ displayVariant,
1098
+ rule.min,
1099
+ bundle.widgetConfig.lowStockThreshold,
1100
+ bundle.widgetConfig.showLowStockBadge
1101
+ ) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1102
+ "Only ",
1103
+ displayVariant.quantityAvailable,
1104
+ " left"
1105
+ ] }),
1016
1106
  showPicker && !selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1017
1107
  const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1018
1108
  value: o.value,
@@ -1031,39 +1121,58 @@ function MixMatchProductRow({
1031
1121
  );
1032
1122
  }) })
1033
1123
  ] }),
1034
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1124
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? showStepper ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1035
1125
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1036
1126
  "button",
1037
1127
  {
1038
1128
  "aria-label": `Decrease ${product.title}`,
1039
- onClick: () => onUpdateQuantity(
1129
+ onClick: () => onSetQuantity(
1040
1130
  product.id,
1041
- displayVariant.id,
1131
+ displayVariant,
1042
1132
  selected.quantity - 1
1043
1133
  ),
1134
+ disabled: selected.quantity <= rule.min,
1044
1135
  children: "\u2212"
1045
1136
  }
1046
1137
  ),
1047
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selected.quantity }),
1138
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-live": "polite", children: selected.quantity }),
1048
1139
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1049
1140
  "button",
1050
1141
  {
1051
1142
  "aria-label": `Increase ${product.title}`,
1052
- onClick: () => onUpdateQuantity(
1143
+ onClick: () => onSetQuantity(
1053
1144
  product.id,
1054
- displayVariant.id,
1145
+ displayVariant,
1055
1146
  selected.quantity + 1
1056
1147
  ),
1148
+ disabled: selected.quantity >= maxAddable,
1057
1149
  children: "+"
1058
1150
  }
1151
+ ),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1153
+ "button",
1154
+ {
1155
+ className: "lb-bundle__remove-btn",
1156
+ "aria-label": `Remove ${product.title}`,
1157
+ onClick: () => onDeselect(product.id, displayVariant.id),
1158
+ children: "Remove"
1159
+ }
1059
1160
  )
1060
1161
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1162
+ "button",
1163
+ {
1164
+ className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
1165
+ "aria-label": `Remove ${product.title}`,
1166
+ onClick: () => onDeselect(product.id, displayVariant.id),
1167
+ children: "Selected"
1168
+ }
1169
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1061
1170
  "button",
1062
1171
  {
1063
1172
  className: "lb-bundle__select-btn",
1064
- onClick: () => onToggle(product.id, displayVariant),
1065
- disabled: !displayVariant.availableForSale,
1066
- children: displayVariant.availableForSale ? "Select" : "Sold out"
1173
+ onClick: () => onSelect(product.id, displayVariant),
1174
+ disabled: !fulfillable || maxAddable <= 0,
1175
+ children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
1067
1176
  }
1068
1177
  ) })
1069
1178
  ]
@@ -1124,7 +1233,8 @@ function VolumeBundle(props) {
1124
1233
  }, [variants]);
1125
1234
  const showPicker = variants.length > 1 && optionNames.length > 0;
1126
1235
  const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1127
- const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
1236
+ const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
1237
+ const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core10.isVariantFulfillable)(v, minTierQty)) ?? variants[0];
1128
1238
  const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1129
1239
  const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
1130
1240
  const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
@@ -1145,7 +1255,9 @@ function VolumeBundle(props) {
1145
1255
  const activeTier = bundle ? (0, import_core10.getActiveTier)(bundle.volumeTiers, quantity) : null;
1146
1256
  const handleAddToCart = (0, import_react8.useCallback)(async () => {
1147
1257
  if (!bundle || !product) return;
1148
- const variant = selectedVariant ?? product.variants.nodes.find((v) => v.availableForSale);
1258
+ const variant = selectedVariant ?? product.variants.nodes.find(
1259
+ (v) => (0, import_core10.isVariantFulfillable)(v, quantity)
1260
+ );
1149
1261
  if (!variant) return;
1150
1262
  const lines = [
1151
1263
  {
@@ -1310,6 +1422,16 @@ function VolumeBundle(props) {
1310
1422
  ] })
1311
1423
  ] }),
1312
1424
  cartError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1425
+ bundle && displayVariant && (0, import_core10.shouldShowLowStockBadge)(
1426
+ displayVariant,
1427
+ minTierQty,
1428
+ bundle.widgetConfig.lowStockThreshold,
1429
+ bundle.widgetConfig.showLowStockBadge
1430
+ ) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1431
+ "Only ",
1432
+ displayVariant.quantityAvailable,
1433
+ " left"
1434
+ ] }),
1313
1435
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1314
1436
  "button",
1315
1437
  {