@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.cjs +137 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +142 -76
- package/dist/index.js.map +1 -1
- package/docs/react-nextjs.md +1 -11
- package/package.json +2 -2
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]);
|
|
@@ -814,6 +834,9 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
|
814
834
|
var import_react7 = require("react");
|
|
815
835
|
var import_core9 = require("@lime-bundles/core");
|
|
816
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
|
+
}
|
|
817
840
|
function MixMatchBundle(props) {
|
|
818
841
|
const {
|
|
819
842
|
shopDomain,
|
|
@@ -857,56 +880,92 @@ function MixMatchBundle(props) {
|
|
|
857
880
|
);
|
|
858
881
|
}
|
|
859
882
|
}, [result, onError]);
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
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]
|
|
863
894
|
);
|
|
864
|
-
const
|
|
865
|
-
const
|
|
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)(
|
|
866
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;
|
|
867
906
|
setSelections((prev) => {
|
|
868
907
|
const next = new Map(prev);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
next.set(key, {
|
|
875
|
-
productId,
|
|
876
|
-
variantId: variant.id,
|
|
877
|
-
quantity: configuredQty
|
|
878
|
-
});
|
|
879
|
-
}
|
|
908
|
+
next.set(`${productId}:${variant.id}`, {
|
|
909
|
+
productId,
|
|
910
|
+
variantId: variant.id,
|
|
911
|
+
quantity: rule.min
|
|
912
|
+
});
|
|
880
913
|
return next;
|
|
881
914
|
});
|
|
882
915
|
},
|
|
883
916
|
[bundle]
|
|
884
917
|
);
|
|
885
|
-
const
|
|
886
|
-
(
|
|
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}`;
|
|
887
932
|
setSelections((prev) => {
|
|
888
933
|
const next = new Map(prev);
|
|
889
|
-
|
|
890
|
-
if (quantity <= 0) {
|
|
934
|
+
if (quantity < rule.min || maxAddable < rule.min) {
|
|
891
935
|
next.delete(key);
|
|
892
|
-
|
|
893
|
-
next.set(key, { productId, variantId, quantity });
|
|
936
|
+
return next;
|
|
894
937
|
}
|
|
938
|
+
const clamped = Math.min(quantity, maxAddable);
|
|
939
|
+
next.set(key, {
|
|
940
|
+
productId,
|
|
941
|
+
variantId: variant.id,
|
|
942
|
+
quantity: clamped
|
|
943
|
+
});
|
|
895
944
|
return next;
|
|
896
945
|
});
|
|
897
946
|
},
|
|
898
|
-
[]
|
|
947
|
+
[bundle]
|
|
899
948
|
);
|
|
900
949
|
const handleAddToCart = (0, import_react7.useCallback)(async () => {
|
|
901
|
-
if (!bundle || !
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
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
|
+
);
|
|
910
969
|
setAddingToCart(true);
|
|
911
970
|
setCartError(null);
|
|
912
971
|
try {
|
|
@@ -932,15 +991,7 @@ function MixMatchBundle(props) {
|
|
|
932
991
|
} finally {
|
|
933
992
|
setAddingToCart(false);
|
|
934
993
|
}
|
|
935
|
-
}, [
|
|
936
|
-
bundle,
|
|
937
|
-
selections,
|
|
938
|
-
validation.valid,
|
|
939
|
-
onAddToCart,
|
|
940
|
-
onError,
|
|
941
|
-
trackAddToCart,
|
|
942
|
-
totalQuantity
|
|
943
|
-
]);
|
|
994
|
+
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
|
|
944
995
|
if (result.status === "loading") {
|
|
945
996
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
946
997
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
@@ -963,27 +1014,29 @@ function MixMatchBundle(props) {
|
|
|
963
1014
|
children: [
|
|
964
1015
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
965
1016
|
bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
966
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__instructions", children:
|
|
1017
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
|
|
967
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)(
|
|
968
1019
|
MixMatchProductRow,
|
|
969
1020
|
{
|
|
970
1021
|
bundle,
|
|
971
1022
|
product,
|
|
972
1023
|
currency,
|
|
1024
|
+
showStepper,
|
|
973
1025
|
selections,
|
|
974
|
-
|
|
975
|
-
|
|
1026
|
+
onSelect: selectVariant,
|
|
1027
|
+
onDeselect: deselect,
|
|
1028
|
+
onSetQuantity: setQuantity
|
|
976
1029
|
},
|
|
977
1030
|
product.id
|
|
978
1031
|
)) }),
|
|
979
|
-
|
|
1032
|
+
validationMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
|
|
980
1033
|
cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
981
1034
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
982
1035
|
"button",
|
|
983
1036
|
{
|
|
984
1037
|
className: "lb-bundle__cta",
|
|
985
1038
|
onClick: handleAddToCart,
|
|
986
|
-
disabled: addingToCart || !
|
|
1039
|
+
disabled: addingToCart || !valid,
|
|
987
1040
|
"aria-busy": addingToCart,
|
|
988
1041
|
children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
|
|
989
1042
|
}
|
|
@@ -996,9 +1049,11 @@ function MixMatchProductRow({
|
|
|
996
1049
|
bundle,
|
|
997
1050
|
product,
|
|
998
1051
|
currency,
|
|
1052
|
+
showStepper,
|
|
999
1053
|
selections,
|
|
1000
|
-
|
|
1001
|
-
|
|
1054
|
+
onSelect,
|
|
1055
|
+
onDeselect,
|
|
1056
|
+
onSetQuantity
|
|
1002
1057
|
}) {
|
|
1003
1058
|
const variants = product.variants.nodes;
|
|
1004
1059
|
const optionNames = (0, import_react7.useMemo)(() => {
|
|
@@ -1006,20 +1061,12 @@ function MixMatchProductRow({
|
|
|
1006
1061
|
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1007
1062
|
}, [variants]);
|
|
1008
1063
|
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1064
|
+
const rule = ruleFor(bundle, product.id);
|
|
1009
1065
|
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1010
|
-
const displayVariant = selectedVariant ?? variants.find(
|
|
1011
|
-
(v) => (0, import_core9.isVariantFulfillable)(v, (0, import_core9.resolveBundleQty)(bundle, product.id, v.id))
|
|
1012
|
-
) ?? variants[0] ?? null;
|
|
1066
|
+
const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core9.isVariantFulfillable)(v, rule.min)) ?? variants[0] ?? null;
|
|
1013
1067
|
if (!displayVariant) return null;
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
product.id,
|
|
1017
|
-
displayVariant.id
|
|
1018
|
-
);
|
|
1019
|
-
const displayVariantFulfillable = (0, import_core9.isVariantFulfillable)(
|
|
1020
|
-
displayVariant,
|
|
1021
|
-
displayVariantQty
|
|
1022
|
-
);
|
|
1068
|
+
const fulfillable = (0, import_core9.isVariantFulfillable)(displayVariant, rule.min);
|
|
1069
|
+
const maxAddable = (0, import_core9.maxAddableQuantity)(displayVariant, rule.max, 0);
|
|
1023
1070
|
const key = `${product.id}:${displayVariant.id}`;
|
|
1024
1071
|
const selected = selections.get(key);
|
|
1025
1072
|
const thumbImage = displayVariant.image ?? product.featuredImage;
|
|
@@ -1048,7 +1095,7 @@ function MixMatchProductRow({
|
|
|
1048
1095
|
unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1049
1096
|
(0, import_core9.shouldShowLowStockBadge)(
|
|
1050
1097
|
displayVariant,
|
|
1051
|
-
|
|
1098
|
+
rule.min,
|
|
1052
1099
|
bundle.widgetConfig.lowStockThreshold,
|
|
1053
1100
|
bundle.widgetConfig.showLowStockBadge
|
|
1054
1101
|
) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
@@ -1074,39 +1121,58 @@ function MixMatchProductRow({
|
|
|
1074
1121
|
);
|
|
1075
1122
|
}) })
|
|
1076
1123
|
] }),
|
|
1077
|
-
/* @__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: [
|
|
1078
1125
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1079
1126
|
"button",
|
|
1080
1127
|
{
|
|
1081
1128
|
"aria-label": `Decrease ${product.title}`,
|
|
1082
|
-
onClick: () =>
|
|
1129
|
+
onClick: () => onSetQuantity(
|
|
1083
1130
|
product.id,
|
|
1084
|
-
displayVariant
|
|
1131
|
+
displayVariant,
|
|
1085
1132
|
selected.quantity - 1
|
|
1086
1133
|
),
|
|
1134
|
+
disabled: selected.quantity <= rule.min,
|
|
1087
1135
|
children: "\u2212"
|
|
1088
1136
|
}
|
|
1089
1137
|
),
|
|
1090
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selected.quantity }),
|
|
1138
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-live": "polite", children: selected.quantity }),
|
|
1091
1139
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1092
1140
|
"button",
|
|
1093
1141
|
{
|
|
1094
1142
|
"aria-label": `Increase ${product.title}`,
|
|
1095
|
-
onClick: () =>
|
|
1143
|
+
onClick: () => onSetQuantity(
|
|
1096
1144
|
product.id,
|
|
1097
|
-
displayVariant
|
|
1145
|
+
displayVariant,
|
|
1098
1146
|
selected.quantity + 1
|
|
1099
1147
|
),
|
|
1148
|
+
disabled: selected.quantity >= maxAddable,
|
|
1100
1149
|
children: "+"
|
|
1101
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
|
+
}
|
|
1102
1160
|
)
|
|
1103
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)(
|
|
1104
1170
|
"button",
|
|
1105
1171
|
{
|
|
1106
1172
|
className: "lb-bundle__select-btn",
|
|
1107
|
-
onClick: () =>
|
|
1108
|
-
disabled: !
|
|
1109
|
-
children:
|
|
1173
|
+
onClick: () => onSelect(product.id, displayVariant),
|
|
1174
|
+
disabled: !fulfillable || maxAddable <= 0,
|
|
1175
|
+
children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
|
|
1110
1176
|
}
|
|
1111
1177
|
) })
|
|
1112
1178
|
]
|