@lime-bundles/react 2.5.0 → 4.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.js CHANGED
@@ -304,6 +304,20 @@ var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = dro
304
304
  var ITEM_HEIGHT_PX = 32;
305
305
  var LIST_PAD_Y = 8;
306
306
  var MAX_VISIBLE_ITEMS = 8;
307
+ function findScrollableAncestor(el) {
308
+ const win = el.ownerDocument?.defaultView;
309
+ if (!win) return null;
310
+ let cur = el.parentElement;
311
+ while (cur && cur !== el.ownerDocument.body) {
312
+ const style = win.getComputedStyle(cur);
313
+ const overflowY = style.overflowY;
314
+ if (overflowY === "auto" || overflowY === "scroll" || overflowY === "hidden") {
315
+ return cur;
316
+ }
317
+ cur = cur.parentElement;
318
+ }
319
+ return null;
320
+ }
307
321
  function VariantDropdown({
308
322
  options,
309
323
  value,
@@ -353,6 +367,11 @@ function VariantDropdown({
353
367
  if (rect.width === 0) return;
354
368
  const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
355
369
  const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
370
+ const scrollable = findScrollableAncestor(trigger);
371
+ const clip = scrollable ? (() => {
372
+ const r = scrollable.getBoundingClientRect();
373
+ return { top: r.top, bottom: r.bottom };
374
+ })() : void 0;
356
375
  setPosition(
357
376
  computePosition({
358
377
  trigger: {
@@ -362,7 +381,8 @@ function VariantDropdown({
362
381
  width: rect.width
363
382
  },
364
383
  viewportHeight: window.innerHeight,
365
- desiredHeight
384
+ desiredHeight,
385
+ clip
366
386
  })
367
387
  );
368
388
  }, [isOpen, options.length]);
@@ -770,12 +790,15 @@ import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMem
770
790
  import {
771
791
  formatMoney as formatMoney2,
772
792
  formatUnitPrice as formatUnitPrice2,
773
- validateQuantity,
774
- resolveBundleQty as resolveBundleQty2,
775
793
  isVariantFulfillable as isVariantFulfillable2,
776
- shouldShowLowStockBadge as shouldShowLowStockBadge2
794
+ shouldShowLowStockBadge as shouldShowLowStockBadge2,
795
+ maxAddableQuantity,
796
+ DEFAULT_PRODUCT_RULE
777
797
  } from "@lime-bundles/core";
778
798
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
799
+ function ruleFor(bundle, productId) {
800
+ return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
801
+ }
779
802
  function MixMatchBundle(props) {
780
803
  const {
781
804
  shopDomain,
@@ -819,56 +842,92 @@ function MixMatchBundle(props) {
819
842
  );
820
843
  }
821
844
  }, [result, onError]);
822
- const totalQuantity = Array.from(selections.values()).reduce(
823
- (sum, s) => sum + s.quantity,
824
- 0
845
+ const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
846
+ const distinctSelectedProducts = useMemo5(() => {
847
+ const ids = /* @__PURE__ */ new Set();
848
+ for (const sel of selections.values()) {
849
+ if (sel.quantity > 0) ids.add(sel.productId);
850
+ }
851
+ return ids;
852
+ }, [selections]);
853
+ const totalQuantity = useMemo5(
854
+ () => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
855
+ [selections]
825
856
  );
826
- const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
827
- const toggleProduct = useCallback5(
857
+ const requiredPicks = bundle?.minQuantity ?? 0;
858
+ const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
859
+ const valid = !!bundle && meetsMinPicks;
860
+ const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
861
+ const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
862
+ const selectVariant = useCallback5(
828
863
  (productId, variant) => {
864
+ if (!bundle) return;
865
+ const rule = ruleFor(bundle, productId);
866
+ const maxAddable = maxAddableQuantity(variant, rule.max, 0);
867
+ if (maxAddable < rule.min) return;
829
868
  setSelections((prev) => {
830
869
  const next = new Map(prev);
831
- const key = `${productId}:${variant.id}`;
832
- if (next.has(key)) {
833
- next.delete(key);
834
- } else {
835
- const configuredQty = bundle ? resolveBundleQty2(bundle, productId, variant.id) : 1;
836
- next.set(key, {
837
- productId,
838
- variantId: variant.id,
839
- quantity: configuredQty
840
- });
841
- }
870
+ next.set(`${productId}:${variant.id}`, {
871
+ productId,
872
+ variantId: variant.id,
873
+ quantity: rule.min
874
+ });
842
875
  return next;
843
876
  });
844
877
  },
845
878
  [bundle]
846
879
  );
847
- const updateQuantity = useCallback5(
848
- (productId, variantId, quantity) => {
880
+ const deselect = useCallback5((productId, variantId) => {
881
+ setSelections((prev) => {
882
+ if (!prev.has(`${productId}:${variantId}`)) return prev;
883
+ const next = new Map(prev);
884
+ next.delete(`${productId}:${variantId}`);
885
+ return next;
886
+ });
887
+ }, []);
888
+ const setQuantity = useCallback5(
889
+ (productId, variant, quantity) => {
890
+ if (!bundle) return;
891
+ const rule = ruleFor(bundle, productId);
892
+ const maxAddable = maxAddableQuantity(variant, rule.max, 0);
893
+ const key = `${productId}:${variant.id}`;
849
894
  setSelections((prev) => {
850
895
  const next = new Map(prev);
851
- const key = `${productId}:${variantId}`;
852
- if (quantity <= 0) {
896
+ if (quantity < rule.min || maxAddable < rule.min) {
853
897
  next.delete(key);
854
- } else {
855
- next.set(key, { productId, variantId, quantity });
898
+ return next;
856
899
  }
900
+ const clamped = Math.min(quantity, maxAddable);
901
+ next.set(key, {
902
+ productId,
903
+ variantId: variant.id,
904
+ quantity: clamped
905
+ });
857
906
  return next;
858
907
  });
859
908
  },
860
- []
909
+ [bundle]
861
910
  );
862
911
  const handleAddToCart = useCallback5(async () => {
863
- if (!bundle || !validation.valid) return;
864
- const lines = Array.from(selections.values()).map((s) => ({
865
- merchandiseId: s.variantId,
866
- quantity: s.quantity,
867
- attributes: [
868
- { key: "_lime_bundle_gid", value: bundle.id },
869
- { key: "_lime_bundle_type", value: bundle.bundleType }
870
- ]
871
- }));
912
+ if (!bundle || !valid) return;
913
+ const byVariant = /* @__PURE__ */ new Map();
914
+ for (const sel of selections.values()) {
915
+ if (sel.quantity <= 0) continue;
916
+ byVariant.set(
917
+ sel.variantId,
918
+ (byVariant.get(sel.variantId) ?? 0) + sel.quantity
919
+ );
920
+ }
921
+ const lines = Array.from(byVariant.entries()).map(
922
+ ([variantId, quantity]) => ({
923
+ merchandiseId: variantId,
924
+ quantity,
925
+ attributes: [
926
+ { key: "_lime_bundle_gid", value: bundle.id },
927
+ { key: "_lime_bundle_type", value: bundle.bundleType }
928
+ ]
929
+ })
930
+ );
872
931
  setAddingToCart(true);
873
932
  setCartError(null);
874
933
  try {
@@ -894,15 +953,7 @@ function MixMatchBundle(props) {
894
953
  } finally {
895
954
  setAddingToCart(false);
896
955
  }
897
- }, [
898
- bundle,
899
- selections,
900
- validation.valid,
901
- onAddToCart,
902
- onError,
903
- trackAddToCart,
904
- totalQuantity
905
- ]);
956
+ }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
906
957
  if (result.status === "loading") {
907
958
  return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
908
959
  /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
@@ -925,27 +976,29 @@ function MixMatchBundle(props) {
925
976
  children: [
926
977
  /* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
927
978
  bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
928
- /* @__PURE__ */ jsx3("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" }),
979
+ /* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
929
980
  /* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
930
981
  MixMatchProductRow,
931
982
  {
932
983
  bundle,
933
984
  product,
934
985
  currency,
986
+ showStepper,
935
987
  selections,
936
- onToggle: toggleProduct,
937
- onUpdateQuantity: updateQuantity
988
+ onSelect: selectVariant,
989
+ onDeselect: deselect,
990
+ onSetQuantity: setQuantity
938
991
  },
939
992
  product.id
940
993
  )) }),
941
- validation.message && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
994
+ validationMessage && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
942
995
  cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
943
996
  /* @__PURE__ */ jsx3(
944
997
  "button",
945
998
  {
946
999
  className: "lb-bundle__cta",
947
1000
  onClick: handleAddToCart,
948
- disabled: addingToCart || !validation.valid,
1001
+ disabled: addingToCart || !valid,
949
1002
  "aria-busy": addingToCart,
950
1003
  children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
951
1004
  }
@@ -958,9 +1011,11 @@ function MixMatchProductRow({
958
1011
  bundle,
959
1012
  product,
960
1013
  currency,
1014
+ showStepper,
961
1015
  selections,
962
- onToggle,
963
- onUpdateQuantity
1016
+ onSelect,
1017
+ onDeselect,
1018
+ onSetQuantity
964
1019
  }) {
965
1020
  const variants = product.variants.nodes;
966
1021
  const optionNames = useMemo5(() => {
@@ -968,20 +1023,12 @@ function MixMatchProductRow({
968
1023
  return first ? first.selectedOptions.map((o) => o.name) : [];
969
1024
  }, [variants]);
970
1025
  const showPicker = variants.length > 1 && optionNames.length > 0;
1026
+ const rule = ruleFor(bundle, product.id);
971
1027
  const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
972
- const displayVariant = selectedVariant ?? variants.find(
973
- (v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, product.id, v.id))
974
- ) ?? variants[0] ?? null;
1028
+ const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? variants[0] ?? null;
975
1029
  if (!displayVariant) return null;
976
- const displayVariantQty = resolveBundleQty2(
977
- bundle,
978
- product.id,
979
- displayVariant.id
980
- );
981
- const displayVariantFulfillable = isVariantFulfillable2(
982
- displayVariant,
983
- displayVariantQty
984
- );
1030
+ const fulfillable = isVariantFulfillable2(displayVariant, rule.min);
1031
+ const maxAddable = maxAddableQuantity(displayVariant, rule.max, 0);
985
1032
  const key = `${product.id}:${displayVariant.id}`;
986
1033
  const selected = selections.get(key);
987
1034
  const thumbImage = displayVariant.image ?? product.featuredImage;
@@ -1010,7 +1057,7 @@ function MixMatchProductRow({
1010
1057
  unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1011
1058
  shouldShowLowStockBadge2(
1012
1059
  displayVariant,
1013
- displayVariantQty,
1060
+ rule.min,
1014
1061
  bundle.widgetConfig.lowStockThreshold,
1015
1062
  bundle.widgetConfig.showLowStockBadge
1016
1063
  ) && /* @__PURE__ */ jsxs3("span", { className: "lb-bundle-low-stock-badge", children: [
@@ -1036,39 +1083,58 @@ function MixMatchProductRow({
1036
1083
  );
1037
1084
  }) })
1038
1085
  ] }),
1039
- /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
1086
+ /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? showStepper ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
1040
1087
  /* @__PURE__ */ jsx3(
1041
1088
  "button",
1042
1089
  {
1043
1090
  "aria-label": `Decrease ${product.title}`,
1044
- onClick: () => onUpdateQuantity(
1091
+ onClick: () => onSetQuantity(
1045
1092
  product.id,
1046
- displayVariant.id,
1093
+ displayVariant,
1047
1094
  selected.quantity - 1
1048
1095
  ),
1096
+ disabled: selected.quantity <= rule.min,
1049
1097
  children: "\u2212"
1050
1098
  }
1051
1099
  ),
1052
- /* @__PURE__ */ jsx3("span", { children: selected.quantity }),
1100
+ /* @__PURE__ */ jsx3("span", { "aria-live": "polite", children: selected.quantity }),
1053
1101
  /* @__PURE__ */ jsx3(
1054
1102
  "button",
1055
1103
  {
1056
1104
  "aria-label": `Increase ${product.title}`,
1057
- onClick: () => onUpdateQuantity(
1105
+ onClick: () => onSetQuantity(
1058
1106
  product.id,
1059
- displayVariant.id,
1107
+ displayVariant,
1060
1108
  selected.quantity + 1
1061
1109
  ),
1110
+ disabled: selected.quantity >= maxAddable,
1062
1111
  children: "+"
1063
1112
  }
1113
+ ),
1114
+ /* @__PURE__ */ jsx3(
1115
+ "button",
1116
+ {
1117
+ className: "lb-bundle__remove-btn",
1118
+ "aria-label": `Remove ${product.title}`,
1119
+ onClick: () => onDeselect(product.id, displayVariant.id),
1120
+ children: "Remove"
1121
+ }
1064
1122
  )
1065
1123
  ] }) : /* @__PURE__ */ jsx3(
1124
+ "button",
1125
+ {
1126
+ className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
1127
+ "aria-label": `Remove ${product.title}`,
1128
+ onClick: () => onDeselect(product.id, displayVariant.id),
1129
+ children: "Selected"
1130
+ }
1131
+ ) : /* @__PURE__ */ jsx3(
1066
1132
  "button",
1067
1133
  {
1068
1134
  className: "lb-bundle__select-btn",
1069
- onClick: () => onToggle(product.id, displayVariant),
1070
- disabled: !displayVariantFulfillable,
1071
- children: displayVariantFulfillable ? "Select" : "Sold out"
1135
+ onClick: () => onSelect(product.id, displayVariant),
1136
+ disabled: !fulfillable || maxAddable <= 0,
1137
+ children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
1072
1138
  }
1073
1139
  ) })
1074
1140
  ]
@@ -1421,7 +1487,7 @@ import {
1421
1487
  computeBundleSaleCents,
1422
1488
  calculateTierSavings as calculateTierSavings2,
1423
1489
  getActiveTier as getActiveTier2,
1424
- validateQuantity as validateQuantity2,
1490
+ validateQuantity,
1425
1491
  transformImageUrl,
1426
1492
  formatCountdown,
1427
1493
  formatMoney as formatMoney4,
@@ -1489,6 +1555,6 @@ export {
1489
1555
  useBundlesForProduct,
1490
1556
  useVariantSelection,
1491
1557
  useWidgetConfigVars,
1492
- validateQuantity2 as validateQuantity
1558
+ validateQuantity
1493
1559
  };
1494
1560
  //# sourceMappingURL=index.js.map