@lime-bundles/react 6.2.0 → 7.1.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
@@ -81,13 +81,10 @@ async function fetchBundleDataWithWarnings(options) {
81
81
  // src/fetchShopSettings.ts
82
82
  import {
83
83
  createStorefrontClient as createStorefrontClient2,
84
- SHOP_SETTINGS_QUERY,
85
- DEFAULT_OUT_OF_STOCK_BEHAVIOR,
86
- parseOutOfStockBehavior
84
+ SHOP_SETTINGS_QUERY
87
85
  } from "@lime-bundles/core";
88
86
  var FALLBACK = {
89
- customCss: null,
90
- outOfStockBehavior: DEFAULT_OUT_OF_STOCK_BEHAVIOR
87
+ customCss: null
91
88
  };
92
89
  var cache = /* @__PURE__ */ new Map();
93
90
  async function fetchShopSettings(options) {
@@ -105,10 +102,7 @@ async function fetchShopSettings(options) {
105
102
  { signal: options.signal }
106
103
  );
107
104
  return {
108
- customCss: data.shop?.customCss?.value ?? null,
109
- outOfStockBehavior: parseOutOfStockBehavior(
110
- data.shop?.outOfStockBehavior?.value
111
- )
105
+ customCss: data.shop?.customCss?.value ?? null
112
106
  };
113
107
  })();
114
108
  cache.set(options.shopDomain, promise);
@@ -266,19 +260,25 @@ function useWidgetConfigVars(config) {
266
260
  import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3, useCallback as useCallback3 } from "react";
267
261
  import { variants as variantsNs } from "@lime-bundles/core";
268
262
  var { findVariantByOptions, isOptionValueAvailable, toPickerVariant } = variantsNs;
269
- function defaultInitial(variants) {
270
- return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
271
- }
272
263
  function useVariantSelection({
273
264
  variants,
274
265
  optionNames,
275
266
  initialVariant,
276
- optionValuesByPosition
267
+ optionValuesByPosition,
268
+ requiredQtyFor
277
269
  }) {
278
- const seed = useMemo2(
279
- () => initialVariant ?? defaultInitial(variants),
280
- [initialVariant, variants]
270
+ const pickerVariants = useMemo2(
271
+ () => variants.map((v) => toPickerVariant(v, requiredQtyFor?.(v.id))),
272
+ [variants, requiredQtyFor]
281
273
  );
274
+ const seed = useMemo2(() => {
275
+ if (initialVariant) return initialVariant;
276
+ const covered = pickerVariants.find((p) => p.available);
277
+ if (covered) {
278
+ return variants.find((v) => v.id === covered.id) ?? null;
279
+ }
280
+ return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
281
+ }, [initialVariant, variants, pickerVariants]);
282
282
  const [selectedValues, setSelectedValues] = useState3(
283
283
  () => seed ? seed.selectedOptions.map((o) => o.value) : optionNames.map(() => "")
284
284
  );
@@ -286,10 +286,6 @@ function useVariantSelection({
286
286
  if (!seed) return;
287
287
  setSelectedValues(seed.selectedOptions.map((o) => o.value));
288
288
  }, [seed?.id]);
289
- const pickerVariants = useMemo2(
290
- () => variants.map((v) => toPickerVariant(v)),
291
- [variants]
292
- );
293
289
  const selectedVariant = useMemo2(() => {
294
290
  if (selectedValues.length !== optionNames.length) return null;
295
291
  const match = findVariantByOptions(pickerVariants, selectedValues);
@@ -924,7 +920,11 @@ function FixedProductRow({ bundle, product, currency, isOos, onVariantChange })
924
920
  const showPicker = variants.length > 1 && optionNames.length > 0;
925
921
  const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
926
922
  variants,
927
- optionNames
923
+ optionNames,
924
+ // Quantity-aware dropdown: a variant that can't cover this row's
925
+ // bundle quantity greys out instead of walking the shopper into a
926
+ // cart Shopify caps at add time.
927
+ requiredQtyFor: (variantId) => resolveBundleQty(bundle, product.id, variantId)
928
928
  });
929
929
  useEffect7(() => {
930
930
  onVariantChange(product.id, selectedVariant ?? null);
@@ -1019,7 +1019,7 @@ function FixedProductRow({ bundle, product, currency, isOos, onVariantChange })
1019
1019
  }
1020
1020
 
1021
1021
  // src/components/MixMatchBundle.tsx
1022
- import { useCallback as useCallback6, useEffect as useEffect12, useMemo as useMemo6, useState as useState9 } from "react";
1022
+ import { useCallback as useCallback6, useEffect as useEffect11, useMemo as useMemo6, useState as useState8 } from "react";
1023
1023
  import {
1024
1024
  formatMoney as formatMoney3,
1025
1025
  formatUnitPrice as formatUnitPrice3,
@@ -1028,39 +1028,12 @@ import {
1028
1028
  DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE2
1029
1029
  } from "@lime-bundles/core";
1030
1030
 
1031
- // src/hooks/useShopSettings.ts
1032
- import { useEffect as useEffect8, useState as useState7 } from "react";
1033
- import {
1034
- DEFAULT_OUT_OF_STOCK_BEHAVIOR as DEFAULT_OUT_OF_STOCK_BEHAVIOR2
1035
- } from "@lime-bundles/core";
1036
- function useShopSettings(options) {
1037
- const [outOfStockBehavior, setOutOfStockBehavior] = useState7(DEFAULT_OUT_OF_STOCK_BEHAVIOR2);
1038
- const { shopDomain, storefrontAccessToken } = options;
1039
- useEffect8(() => {
1040
- const controller = new AbortController();
1041
- let active = true;
1042
- fetchShopSettingsOrDefault({
1043
- shopDomain,
1044
- storefrontAccessToken,
1045
- signal: controller.signal
1046
- }).then((settings) => {
1047
- if (!active || controller.signal.aborted) return;
1048
- setOutOfStockBehavior(settings.outOfStockBehavior);
1049
- });
1050
- return () => {
1051
- active = false;
1052
- controller.abort();
1053
- };
1054
- }, [shopDomain, storefrontAccessToken]);
1055
- return { outOfStockBehavior };
1056
- }
1057
-
1058
1031
  // src/components/MixMatchPicker.tsx
1059
1032
  import {
1060
- useEffect as useEffect11,
1033
+ useEffect as useEffect10,
1061
1034
  useMemo as useMemo5,
1062
1035
  useRef as useRef6,
1063
- useState as useState8
1036
+ useState as useState7
1064
1037
  } from "react";
1065
1038
  import { createPortal } from "react-dom";
1066
1039
  import {
@@ -1072,7 +1045,7 @@ import {
1072
1045
  } from "@lime-bundles/core";
1073
1046
 
1074
1047
  // src/hooks/useScrollLock.ts
1075
- import { useEffect as useEffect9 } from "react";
1048
+ import { useEffect as useEffect8 } from "react";
1076
1049
  var lockCount = 0;
1077
1050
  var savedY = 0;
1078
1051
  function lock() {
@@ -1101,7 +1074,7 @@ function unlock() {
1101
1074
  window.scrollTo(0, savedY);
1102
1075
  }
1103
1076
  function useScrollLock(active) {
1104
- useEffect9(() => {
1077
+ useEffect8(() => {
1105
1078
  if (!active) return;
1106
1079
  lock();
1107
1080
  return unlock;
@@ -1109,7 +1082,7 @@ function useScrollLock(active) {
1109
1082
  }
1110
1083
 
1111
1084
  // src/hooks/useFocusTrap.ts
1112
- import { useEffect as useEffect10, useRef as useRef5 } from "react";
1085
+ import { useEffect as useEffect9, useRef as useRef5 } from "react";
1113
1086
  var FOCUSABLE_SELECTOR = [
1114
1087
  "a[href]",
1115
1088
  "button:not([disabled])",
@@ -1132,14 +1105,16 @@ function useFocusTrap({
1132
1105
  active,
1133
1106
  containerRef,
1134
1107
  onEscape,
1108
+ initialFocusRef,
1135
1109
  returnFocusRef
1136
1110
  }) {
1137
1111
  const previouslyFocused = useRef5(null);
1138
- useEffect10(() => {
1112
+ useEffect9(() => {
1139
1113
  if (!active) return;
1140
1114
  const container = containerRef.current;
1141
1115
  previouslyFocused.current = document.activeElement ?? null;
1142
- container?.focus();
1116
+ const initial = initialFocusRef?.current ?? container;
1117
+ initial?.focus();
1143
1118
  function onKeyDown(e) {
1144
1119
  if (e.key === "Escape") {
1145
1120
  e.preventDefault();
@@ -1191,7 +1166,7 @@ function variantRuleFor(bundle, variantId) {
1191
1166
  function sanitizeId(gid) {
1192
1167
  return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
1193
1168
  }
1194
- function buildEligibleProducts(bundle, oosBehavior) {
1169
+ function buildEligibleProducts(bundle) {
1195
1170
  const result = [];
1196
1171
  const seen = /* @__PURE__ */ new Set();
1197
1172
  for (const product of bundle.products) {
@@ -1202,7 +1177,6 @@ function buildEligibleProducts(bundle, oosBehavior) {
1202
1177
  (v) => isVariantFulfillable2(v, rule.min)
1203
1178
  );
1204
1179
  const isOos = available.length === 0;
1205
- if (isOos && oosBehavior === "hide") continue;
1206
1180
  result.push({
1207
1181
  product,
1208
1182
  variants: product.variants.nodes,
@@ -1217,7 +1191,6 @@ function normalizeText(str) {
1217
1191
  }
1218
1192
  var FILTERS_ALL = "__all__";
1219
1193
  function MixMatchPicker({
1220
- outOfStockBehavior,
1221
1194
  bundle,
1222
1195
  currency,
1223
1196
  requiredQty,
@@ -1235,13 +1208,14 @@ function MixMatchPicker({
1235
1208
  const wc = bundle.widgetConfig;
1236
1209
  const overlayRef = useRef6(null);
1237
1210
  const dialogRef = useRef6(null);
1211
+ const closeBtnRef = useRef6(null);
1238
1212
  const mouseDownTarget = useRef6(null);
1239
- const [open, setOpen] = useState8(false);
1240
- const [query, setQuery] = useState8("");
1241
- const [activeType, setActiveType] = useState8(FILTERS_ALL);
1213
+ const [open, setOpen] = useState7(false);
1214
+ const [query, setQuery] = useState7("");
1215
+ const [activeType, setActiveType] = useState7(FILTERS_ALL);
1242
1216
  const eligible = useMemo5(
1243
- () => buildEligibleProducts(bundle, outOfStockBehavior),
1244
- [bundle, outOfStockBehavior]
1217
+ () => buildEligibleProducts(bundle),
1218
+ [bundle]
1245
1219
  );
1246
1220
  const productTypes = useMemo5(() => {
1247
1221
  const types = [];
@@ -1259,7 +1233,7 @@ function MixMatchPicker({
1259
1233
  const totalUnits = selections.reduce((sum, s) => sum + s.quantity, 0);
1260
1234
  const atCapacity = totalUnits >= requiredQty;
1261
1235
  const shownUnits = Math.min(totalUnits, requiredQty);
1262
- useEffect11(() => {
1236
+ useEffect10(() => {
1263
1237
  const id = requestAnimationFrame(() => setOpen(true));
1264
1238
  return () => cancelAnimationFrame(id);
1265
1239
  }, []);
@@ -1267,6 +1241,7 @@ function MixMatchPicker({
1267
1241
  useFocusTrap({
1268
1242
  active: true,
1269
1243
  containerRef: dialogRef,
1244
+ initialFocusRef: closeBtnRef,
1270
1245
  onEscape: onClose
1271
1246
  });
1272
1247
  const remaining = Math.max(0, requiredQty - shownUnits);
@@ -1275,14 +1250,25 @@ function MixMatchPicker({
1275
1250
  const pct = bundle.discountConfig.discountType === "percentage" && bundle.discountConfig.discountValue ? Math.round(bundle.discountConfig.discountValue) : 0;
1276
1251
  return pct > 0 ? `Choose ${remaining} more to unlock ${pct}% off` : `Choose ${remaining} more to complete your bundle`;
1277
1252
  })();
1253
+ const [selectedAtOpen] = useState7(
1254
+ () => new Set(selections.map((s) => s.productId))
1255
+ );
1256
+ const ordered = useMemo5(() => {
1257
+ const front = [];
1258
+ const back = [];
1259
+ for (const ep of eligible) {
1260
+ (selectedAtOpen.has(ep.product.id) ? back : front).push(ep);
1261
+ }
1262
+ return front.concat(back);
1263
+ }, [eligible, selectedAtOpen]);
1278
1264
  const filtered = useMemo5(() => {
1279
1265
  const nq = normalizeText(query.trim());
1280
- return eligible.filter((ep) => {
1266
+ return ordered.filter((ep) => {
1281
1267
  const matchesQuery = !nq || normalizeText(ep.product.title).includes(nq);
1282
1268
  const matchesType = activeType === FILTERS_ALL || (ep.product.productType ?? "") === activeType;
1283
1269
  return matchesQuery && matchesType;
1284
1270
  });
1285
- }, [eligible, query, activeType]);
1271
+ }, [ordered, query, activeType]);
1286
1272
  const showEmpty = filtered.length === 0 && query.trim().length > 0;
1287
1273
  function onOverlayMouseDown(e) {
1288
1274
  mouseDownTarget.current = e.target;
@@ -1331,6 +1317,7 @@ function MixMatchPicker({
1331
1317
  /* @__PURE__ */ jsx4(
1332
1318
  "button",
1333
1319
  {
1320
+ ref: closeBtnRef,
1334
1321
  type: "button",
1335
1322
  className: "lb-mix-match__modal-close",
1336
1323
  "data-modal-close": true,
@@ -1533,12 +1520,12 @@ function MixMatchPickerRow({
1533
1520
  Math.max(0, alreadyInBundle - committedQty)
1534
1521
  ) : 0;
1535
1522
  const stepperMax = Math.max(vRule.min, cap);
1536
- const [qty, setQty] = useState8(vRule.min);
1537
- useEffect11(() => {
1523
+ const [qty, setQty] = useState7(vRule.min);
1524
+ useEffect10(() => {
1538
1525
  setQty((prev) => Math.max(vRule.min, Math.min(prev, stepperMax)));
1539
1526
  }, [stepperMax, vRule.min]);
1540
1527
  const currentVariantId = currentVariant?.id ?? null;
1541
- useEffect11(() => {
1528
+ useEffect10(() => {
1542
1529
  if (!variantInBundle) setQty(vRule.min);
1543
1530
  }, [variantInBundle, currentVariantId, vRule.min]);
1544
1531
  if (!currentVariant) return null;
@@ -1800,10 +1787,6 @@ function MixMatchBundle(props) {
1800
1787
  storefrontAccessToken,
1801
1788
  bundleGid
1802
1789
  });
1803
- const { outOfStockBehavior } = useShopSettings({
1804
- shopDomain,
1805
- storefrontAccessToken
1806
- });
1807
1790
  const { elementRef, trackAddToCart } = useAnalytics({
1808
1791
  shopDomain,
1809
1792
  appUrl: appUrl ?? `https://${shopDomain}`,
@@ -1811,16 +1794,16 @@ function MixMatchBundle(props) {
1811
1794
  bundleType: "mix_match",
1812
1795
  enabled: analyticsEnabled !== false
1813
1796
  });
1814
- const [selections, setSelections] = useState9([]);
1815
- const [seededFor, setSeededFor] = useState9(null);
1816
- const [pickerOpen, setPickerOpen] = useState9(false);
1817
- const [addingToCart, setAddingToCart] = useState9(false);
1818
- const [cartError, setCartError] = useState9(null);
1797
+ const [selections, setSelections] = useState8([]);
1798
+ const [seededFor, setSeededFor] = useState8(null);
1799
+ const [pickerOpen, setPickerOpen] = useState8(false);
1800
+ const [addingToCart, setAddingToCart] = useState8(false);
1801
+ const [cartError, setCartError] = useState8(null);
1819
1802
  const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
1820
1803
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1821
1804
  const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1822
1805
  const slotsEdgeFadeRef = useEdgeFade();
1823
- useEffect12(() => {
1806
+ useEffect11(() => {
1824
1807
  if (result.status === "error") {
1825
1808
  onError?.(result.error);
1826
1809
  return;
@@ -1833,7 +1816,7 @@ function MixMatchBundle(props) {
1833
1816
  );
1834
1817
  }
1835
1818
  }, [result, onError]);
1836
- useEffect12(() => {
1819
+ useEffect11(() => {
1837
1820
  if (!bundle || seededFor === bundle.id) return;
1838
1821
  setSeededFor(bundle.id);
1839
1822
  const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
@@ -2243,7 +2226,6 @@ function MixMatchBundle(props) {
2243
2226
  pickerOpen && /* @__PURE__ */ jsx5(
2244
2227
  MixMatchPicker,
2245
2228
  {
2246
- outOfStockBehavior,
2247
2229
  bundle,
2248
2230
  currency,
2249
2231
  requiredQty: requiredUnits,
@@ -2316,7 +2298,7 @@ function CloseIcon2() {
2316
2298
  }
2317
2299
 
2318
2300
  // src/components/VolumeBundle.tsx
2319
- import { useCallback as useCallback7, useEffect as useEffect13, useMemo as useMemo7, useState as useState10 } from "react";
2301
+ import { useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo7, useState as useState9 } from "react";
2320
2302
  import {
2321
2303
  formatMoney as formatMoney4,
2322
2304
  formatUnitPrice as formatUnitPrice4,
@@ -2327,6 +2309,7 @@ import {
2327
2309
  resolveDefaultTierIndex,
2328
2310
  resolvePopularTierIndex,
2329
2311
  isVariantFulfillable as isVariantFulfillable4,
2312
+ resolveContextProduct,
2330
2313
  shouldShowLowStockBadge as shouldShowLowStockBadge2
2331
2314
  } from "@lime-bundles/core";
2332
2315
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -2339,7 +2322,8 @@ function VolumeBundle(props) {
2339
2322
  analyticsEnabled,
2340
2323
  onAddToCart,
2341
2324
  onError,
2342
- className
2325
+ className,
2326
+ productId
2343
2327
  } = props;
2344
2328
  const result = useBundleData({
2345
2329
  shopDomain,
@@ -2353,12 +2337,12 @@ function VolumeBundle(props) {
2353
2337
  bundleType: "volume",
2354
2338
  enabled: analyticsEnabled !== false
2355
2339
  });
2356
- const [quantity, setQuantity] = useState10(1);
2357
- const [addingToCart, setAddingToCart] = useState10(false);
2358
- const [cartError, setCartError] = useState10(null);
2359
- const [seededFor, setSeededFor] = useState10(null);
2340
+ const [quantity, setQuantity] = useState9(1);
2341
+ const [addingToCart, setAddingToCart] = useState9(false);
2342
+ const [cartError, setCartError] = useState9(null);
2343
+ const [seededFor, setSeededFor] = useState9(null);
2360
2344
  const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
2361
- useEffect13(() => {
2345
+ useEffect12(() => {
2362
2346
  if (!bundle || seededFor === bundle.id) return;
2363
2347
  setSeededFor(bundle.id);
2364
2348
  const tiers = bundle.volumeTiers;
@@ -2376,7 +2360,7 @@ function VolumeBundle(props) {
2376
2360
  }, [bundle, seededFor]);
2377
2361
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
2378
2362
  const tiersEdgeFadeRef = useEdgeFade();
2379
- useEffect13(() => {
2363
+ useEffect12(() => {
2380
2364
  if (result.status === "error") {
2381
2365
  onError?.(result.error);
2382
2366
  return;
@@ -2389,7 +2373,7 @@ function VolumeBundle(props) {
2389
2373
  );
2390
2374
  }
2391
2375
  }, [result, onError]);
2392
- const product = bundle?.products[0];
2376
+ const product = bundle ? resolveContextProduct(bundle.products, { productId }) : void 0;
2393
2377
  const variants = useMemo7(() => product?.variants.nodes ?? [], [product]);
2394
2378
  const optionNames = useMemo7(() => {
2395
2379
  const first = variants[0];
@@ -2400,6 +2384,11 @@ function VolumeBundle(props) {
2400
2384
  const minTierQty = bundle ? getMinTierQuantity(bundle.volumeTiers) : 1;
2401
2385
  const isSoldOut = variants.length > 0 && !variants.some((v) => isVariantFulfillable4(v, minTierQty));
2402
2386
  const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable4(v, minTierQty)) ?? variants[0];
2387
+ const tierAvailable = useCallback7(
2388
+ (tierQty) => Boolean(displayVariant && isVariantFulfillable4(displayVariant, tierQty)),
2389
+ [displayVariant]
2390
+ );
2391
+ const selectedTierUnavailable = !isSoldOut && !tierAvailable(quantity);
2403
2392
  const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
2404
2393
  const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
2405
2394
  const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
@@ -2557,16 +2546,20 @@ function VolumeBundle(props) {
2557
2546
  const savingsPercent = Math.round(ts.savingsPercent);
2558
2547
  const showCompare = bundle.widgetConfig.pricing.showComparePrice && ts.savings > 0;
2559
2548
  const isChecked = activeTier === ts.tier;
2549
+ const available = tierAvailable(ts.tier.minQuantity);
2560
2550
  return /* @__PURE__ */ jsxs6(
2561
2551
  "div",
2562
2552
  {
2563
- className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
2553
+ className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""} ${available ? "" : "lb-bundle__tier--oos"}`,
2564
2554
  role: "radio",
2565
2555
  "aria-checked": isChecked,
2556
+ "aria-disabled": available ? void 0 : true,
2566
2557
  tabIndex: isChecked ? 0 : -1,
2567
- onClick: () => setQuantity(ts.tier.minQuantity),
2558
+ onClick: () => {
2559
+ if (available) setQuantity(ts.tier.minQuantity);
2560
+ },
2568
2561
  onKeyDown: (e) => {
2569
- if (e.key === "Enter" || e.key === " ") {
2562
+ if ((e.key === "Enter" || e.key === " ") && available) {
2570
2563
  e.preventDefault();
2571
2564
  setQuantity(ts.tier.minQuantity);
2572
2565
  }
@@ -2681,9 +2674,9 @@ function VolumeBundle(props) {
2681
2674
  {
2682
2675
  className: "lb-bundle__cta",
2683
2676
  onClick: handleAddToCart,
2684
- disabled: addingToCart || isSoldOut,
2677
+ disabled: addingToCart || isSoldOut || selectedTierUnavailable,
2685
2678
  "aria-busy": addingToCart,
2686
- children: addingToCart ? "Adding..." : isSoldOut ? "Sold out" : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
2679
+ children: addingToCart ? "Adding..." : isSoldOut || selectedTierUnavailable ? "Sold out" : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
2687
2680
  }
2688
2681
  )
2689
2682
  ]
@@ -2692,7 +2685,7 @@ function VolumeBundle(props) {
2692
2685
  }
2693
2686
 
2694
2687
  // src/components/BogoBundle.tsx
2695
- import { useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo8, useState as useState11 } from "react";
2688
+ import { useCallback as useCallback8, useEffect as useEffect13, useMemo as useMemo8, useState as useState10 } from "react";
2696
2689
  import {
2697
2690
  formatMoney as formatMoney5,
2698
2691
  formatUnitPrice as formatUnitPrice5,
@@ -2727,9 +2720,9 @@ function BogoBundle(props) {
2727
2720
  bundleType: "bogo",
2728
2721
  enabled: analyticsEnabled !== false
2729
2722
  });
2730
- const [addingToCart, setAddingToCart] = useState11(false);
2731
- const [cartError, setCartError] = useState11(null);
2732
- const [selectedVariants, setSelectedVariants] = useState11({ buy: null, get: null });
2723
+ const [addingToCart, setAddingToCart] = useState10(false);
2724
+ const [cartError, setCartError] = useState10(null);
2725
+ const [selectedVariants, setSelectedVariants] = useState10({ buy: null, get: null });
2733
2726
  const handleVariantChange = useCallback8(
2734
2727
  (role, variant) => {
2735
2728
  setSelectedVariants((prev) => {
@@ -2741,7 +2734,7 @@ function BogoBundle(props) {
2741
2734
  );
2742
2735
  const bundle = result.status === "success" && result.bundle.bundleType === "bogo" ? result.bundle : null;
2743
2736
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
2744
- useEffect14(() => {
2737
+ useEffect13(() => {
2745
2738
  if (result.status === "error") {
2746
2739
  onError?.(result.error);
2747
2740
  return;
@@ -2940,7 +2933,7 @@ function BogoProductRow({ side, product, quantity, percent, badge, currency, onV
2940
2933
  variants,
2941
2934
  optionNames
2942
2935
  });
2943
- useEffect14(() => {
2936
+ useEffect13(() => {
2944
2937
  onVariantChange(side, selectedVariant ?? null);
2945
2938
  }, [selectedVariant, side, onVariantChange]);
2946
2939
  const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable5(v, quantity)) ?? variants[0];
@@ -3013,7 +3006,7 @@ function BogoProductRow({ side, product, quantity, percent, badge, currency, onV
3013
3006
  }
3014
3007
 
3015
3008
  // src/components/MultiStepBundle.tsx
3016
- import { useCallback as useCallback9, useEffect as useEffect16, useMemo as useMemo10, useState as useState13 } from "react";
3009
+ import { useCallback as useCallback9, useEffect as useEffect15, useMemo as useMemo10, useState as useState12 } from "react";
3017
3010
  import {
3018
3011
  formatMoney as formatMoney7,
3019
3012
  formatUnitPrice as formatUnitPrice7,
@@ -3026,10 +3019,10 @@ import {
3026
3019
 
3027
3020
  // src/components/MultiStepPicker.tsx
3028
3021
  import {
3029
- useEffect as useEffect15,
3022
+ useEffect as useEffect14,
3030
3023
  useMemo as useMemo9,
3031
3024
  useRef as useRef7,
3032
- useState as useState12
3025
+ useState as useState11
3033
3026
  } from "react";
3034
3027
  import { createPortal as createPortal2 } from "react-dom";
3035
3028
  import {
@@ -3055,7 +3048,7 @@ function variantRuleFor3(bundle, variantId) {
3055
3048
  function sanitizeId2(gid) {
3056
3049
  return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
3057
3050
  }
3058
- function buildEligibleProducts2(bundle, stepIndex, oosBehavior) {
3051
+ function buildEligibleProducts2(bundle, stepIndex) {
3059
3052
  const result = [];
3060
3053
  for (const product of productsForStep(bundle, stepIndex)) {
3061
3054
  const rule = ruleFor3(bundle, product.id);
@@ -3063,7 +3056,6 @@ function buildEligibleProducts2(bundle, stepIndex, oosBehavior) {
3063
3056
  (v) => isVariantFulfillable6(v, rule.min)
3064
3057
  );
3065
3058
  const isOos = available.length === 0;
3066
- if (isOos && oosBehavior === "hide") continue;
3067
3059
  result.push({
3068
3060
  product,
3069
3061
  variants: product.variants.nodes,
@@ -3078,7 +3070,6 @@ function normalizeText2(str) {
3078
3070
  }
3079
3071
  var FILTERS_ALL2 = "__all__";
3080
3072
  function MultiStepPicker({
3081
- outOfStockBehavior,
3082
3073
  bundle,
3083
3074
  currency,
3084
3075
  initialStep,
@@ -3098,14 +3089,16 @@ function MultiStepPicker({
3098
3089
  const overlayRef = useRef7(null);
3099
3090
  const dialogRef = useRef7(null);
3100
3091
  const titleRef = useRef7(null);
3092
+ const closeBtnRef = useRef7(null);
3093
+ const listRef = useRef7(null);
3101
3094
  const mouseDownTarget = useRef7(null);
3102
- const [open, setOpen] = useState12(false);
3103
- const [stepIndex, setStepIndex] = useState12(
3095
+ const [open, setOpen] = useState11(false);
3096
+ const [stepIndex, setStepIndex] = useState11(
3104
3097
  () => Math.max(0, Math.min(steps.length - 1, initialStep))
3105
3098
  );
3106
- const [query, setQuery] = useState12("");
3107
- const [activeType, setActiveType] = useState12(FILTERS_ALL2);
3108
- const [announcement, setAnnouncement] = useState12("");
3099
+ const [query, setQuery] = useState11("");
3100
+ const [activeType, setActiveType] = useState11(FILTERS_ALL2);
3101
+ const [announcement, setAnnouncement] = useState11("");
3109
3102
  const step = steps[stepIndex];
3110
3103
  const stepMin = step?.minQuantity ?? 1;
3111
3104
  const stepPicks = selections[stepIndex] ?? [];
@@ -3114,8 +3107,8 @@ function MultiStepPicker({
3114
3107
  const isLastStep = stepIndex === steps.length - 1;
3115
3108
  const shownUnits = Math.min(unitsInStep, stepMin);
3116
3109
  const eligible = useMemo9(
3117
- () => buildEligibleProducts2(bundle, stepIndex, outOfStockBehavior),
3118
- [bundle, stepIndex, outOfStockBehavior]
3110
+ () => buildEligibleProducts2(bundle, stepIndex),
3111
+ [bundle, stepIndex]
3119
3112
  );
3120
3113
  const productTypes = useMemo9(() => {
3121
3114
  const types = [];
@@ -3130,7 +3123,7 @@ function MultiStepPicker({
3130
3123
  return types;
3131
3124
  }, [eligible]);
3132
3125
  const showFilters = wc.mixMatchShowTypeFilters && productTypes.length >= 2;
3133
- useEffect15(() => {
3126
+ useEffect14(() => {
3134
3127
  const id = requestAnimationFrame(() => setOpen(true));
3135
3128
  return () => cancelAnimationFrame(id);
3136
3129
  }, []);
@@ -3138,8 +3131,23 @@ function MultiStepPicker({
3138
3131
  useFocusTrap({
3139
3132
  active: true,
3140
3133
  containerRef: dialogRef,
3134
+ initialFocusRef: closeBtnRef,
3141
3135
  onEscape: onClose
3142
3136
  });
3137
+ useEffect14(() => {
3138
+ if (listRef.current) listRef.current.scrollTop = 0;
3139
+ }, [stepIndex]);
3140
+ const [selectedAtLoad, setSelectedAtLoad] = useState11(
3141
+ () => new Set((selections[stepIndex] ?? []).map((s) => s.productId))
3142
+ );
3143
+ const ordered = useMemo9(() => {
3144
+ const front = [];
3145
+ const back = [];
3146
+ for (const ep of eligible) {
3147
+ (selectedAtLoad.has(ep.product.id) ? back : front).push(ep);
3148
+ }
3149
+ return front.concat(back);
3150
+ }, [eligible, selectedAtLoad]);
3143
3151
  function goToStep(i) {
3144
3152
  const clamped = Math.max(0, Math.min(steps.length - 1, i));
3145
3153
  setStepIndex(clamped);
@@ -3147,6 +3155,7 @@ function MultiStepPicker({
3147
3155
  setActiveType(FILTERS_ALL2);
3148
3156
  const target = steps[clamped];
3149
3157
  const targetPicks = selections[clamped] ?? [];
3158
+ setSelectedAtLoad(new Set(targetPicks.map((s) => s.productId)));
3150
3159
  const targetUnits = targetPicks.reduce((sum, s) => sum + s.quantity, 0);
3151
3160
  setAnnouncement(
3152
3161
  `Step ${clamped + 1} of ${steps.length}. ${target.name}. ${Math.min(targetUnits, target.minQuantity)} of ${target.minQuantity} added`
@@ -3156,12 +3165,12 @@ function MultiStepPicker({
3156
3165
  const subtitle = `Step ${stepIndex + 1} of ${steps.length} \xB7 ${step?.name ?? ""}`;
3157
3166
  const filtered = useMemo9(() => {
3158
3167
  const nq = normalizeText2(query.trim());
3159
- return eligible.filter((ep) => {
3168
+ return ordered.filter((ep) => {
3160
3169
  const matchesQuery = !nq || normalizeText2(ep.product.title).includes(nq);
3161
3170
  const matchesType = activeType === FILTERS_ALL2 || (ep.product.productType ?? "") === activeType;
3162
3171
  return matchesQuery && matchesType;
3163
3172
  });
3164
- }, [eligible, query, activeType]);
3173
+ }, [ordered, query, activeType]);
3165
3174
  const showEmpty = filtered.length === 0 && query.trim().length > 0;
3166
3175
  function onOverlayMouseDown(e) {
3167
3176
  mouseDownTarget.current = e.target;
@@ -3219,6 +3228,7 @@ function MultiStepPicker({
3219
3228
  /* @__PURE__ */ jsx8(
3220
3229
  "button",
3221
3230
  {
3231
+ ref: closeBtnRef,
3222
3232
  type: "button",
3223
3233
  className: "lb-mix-match__modal-close",
3224
3234
  "data-modal-close": true,
@@ -3306,7 +3316,7 @@ function MultiStepPicker({
3306
3316
  })
3307
3317
  }
3308
3318
  ),
3309
- /* @__PURE__ */ jsx8("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, children: filtered.map((ep) => /* @__PURE__ */ jsx8(
3319
+ /* @__PURE__ */ jsx8("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, ref: listRef, children: filtered.map((ep) => /* @__PURE__ */ jsx8(
3310
3320
  MultiStepPickerRow,
3311
3321
  {
3312
3322
  bundle,
@@ -3453,12 +3463,12 @@ function MultiStepPickerRow({
3453
3463
  const stockRemaining = currentVariant ? maxAddableQuantity2(currentVariant, Number.MAX_SAFE_INTEGER, variantOtherUnits) : 0;
3454
3464
  const cap = Math.max(0, Math.min(spotCeiling, stockRemaining));
3455
3465
  const stepperMax = Math.max(vRule.min, cap);
3456
- const [qty, setQty] = useState12(vRule.min);
3457
- useEffect15(() => {
3466
+ const [qty, setQty] = useState11(vRule.min);
3467
+ useEffect14(() => {
3458
3468
  setQty((prev) => Math.max(vRule.min, Math.min(prev, stepperMax)));
3459
3469
  }, [stepperMax, vRule.min]);
3460
3470
  const currentVariantId = currentVariant?.id ?? null;
3461
- useEffect15(() => {
3471
+ useEffect14(() => {
3462
3472
  if (!variantInStep) setQty(vRule.min);
3463
3473
  }, [variantInStep, currentVariantId, vRule.min]);
3464
3474
  if (!currentVariant || !step) return null;
@@ -3729,10 +3739,6 @@ function MultiStepBundle(props) {
3729
3739
  storefrontAccessToken,
3730
3740
  bundleGid
3731
3741
  });
3732
- const { outOfStockBehavior } = useShopSettings({
3733
- shopDomain,
3734
- storefrontAccessToken
3735
- });
3736
3742
  const { elementRef, trackAddToCart } = useAnalytics({
3737
3743
  shopDomain,
3738
3744
  appUrl: appUrl ?? `https://${shopDomain}`,
@@ -3740,14 +3746,14 @@ function MultiStepBundle(props) {
3740
3746
  bundleType: "multi_step",
3741
3747
  enabled: analyticsEnabled !== false
3742
3748
  });
3743
- const [selections, setSelections] = useState13([]);
3744
- const [pickerOpenAt, setPickerOpenAt] = useState13(null);
3745
- const [addingToCart, setAddingToCart] = useState13(false);
3746
- const [cartError, setCartError] = useState13(null);
3749
+ const [selections, setSelections] = useState12([]);
3750
+ const [pickerOpenAt, setPickerOpenAt] = useState12(null);
3751
+ const [addingToCart, setAddingToCart] = useState12(false);
3752
+ const [cartError, setCartError] = useState12(null);
3747
3753
  const bundle = result.status === "success" && result.bundle.bundleType === "multi_step" ? result.bundle : null;
3748
3754
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
3749
3755
  const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
3750
- useEffect16(() => {
3756
+ useEffect15(() => {
3751
3757
  if (result.status === "error") {
3752
3758
  onError?.(result.error);
3753
3759
  return;
@@ -3761,7 +3767,7 @@ function MultiStepBundle(props) {
3761
3767
  }
3762
3768
  }, [result, onError]);
3763
3769
  const steps = useMemo10(() => bundle?.steps ?? [], [bundle]);
3764
- useEffect16(() => {
3770
+ useEffect15(() => {
3765
3771
  const seeded = steps.map(() => []);
3766
3772
  if (bundle) {
3767
3773
  const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
@@ -4201,7 +4207,6 @@ function MultiStepBundle(props) {
4201
4207
  pickerOpenAt !== null && /* @__PURE__ */ jsx9(
4202
4208
  MultiStepPicker,
4203
4209
  {
4204
- outOfStockBehavior,
4205
4210
  bundle,
4206
4211
  currency,
4207
4212
  initialStep: pickerOpenAt,
@@ -4275,7 +4280,7 @@ async function fetchShopCustomCss(options) {
4275
4280
  }
4276
4281
 
4277
4282
  // src/hooks/useBundlesForProduct.ts
4278
- import { useState as useState14, useEffect as useEffect17 } from "react";
4283
+ import { useState as useState13, useEffect as useEffect16 } from "react";
4279
4284
  import {
4280
4285
  fetchBundlesForProduct,
4281
4286
  injectCustomCss as injectCustomCss2
@@ -4286,10 +4291,10 @@ var INITIAL_STATE2 = {
4286
4291
  error: null
4287
4292
  };
4288
4293
  function useBundlesForProduct(options) {
4289
- const [state, setState] = useState14(
4294
+ const [state, setState] = useState13(
4290
4295
  INITIAL_STATE2
4291
4296
  );
4292
- useEffect17(() => {
4297
+ useEffect16(() => {
4293
4298
  const controller = new AbortController();
4294
4299
  setState(INITIAL_STATE2);
4295
4300
  fetchBundlesForProduct({
@@ -4363,9 +4368,7 @@ import {
4363
4368
  parseMetaobjectBundleStrict as parseMetaobjectBundleStrict2,
4364
4369
  BUNDLE_METAOBJECT_QUERY as BUNDLE_METAOBJECT_QUERY2,
4365
4370
  BUNDLES_FOR_PRODUCT_QUERY,
4366
- SHOP_SETTINGS_QUERY as SHOP_SETTINGS_QUERY2,
4367
- DEFAULT_OUT_OF_STOCK_BEHAVIOR as DEFAULT_OUT_OF_STOCK_BEHAVIOR3,
4368
- parseOutOfStockBehavior as parseOutOfStockBehavior2
4371
+ SHOP_SETTINGS_QUERY as SHOP_SETTINGS_QUERY2
4369
4372
  } from "@lime-bundles/core";
4370
4373
  export {
4371
4374
  BUNDLES_FOR_PRODUCT_QUERY,
@@ -4373,7 +4376,6 @@ export {
4373
4376
  BogoBundle,
4374
4377
  BundleCountdown,
4375
4378
  BundleParseError2 as BundleParseError,
4376
- DEFAULT_OUT_OF_STOCK_BEHAVIOR3 as DEFAULT_OUT_OF_STOCK_BEHAVIOR,
4377
4379
  FixedBundle,
4378
4380
  MixMatchBundle,
4379
4381
  MultiStepBundle,
@@ -4404,7 +4406,6 @@ export {
4404
4406
  parseCents,
4405
4407
  parseMetaobjectBundle,
4406
4408
  parseMetaobjectBundleStrict2 as parseMetaobjectBundleStrict,
4407
- parseOutOfStockBehavior2 as parseOutOfStockBehavior,
4408
4409
  percentageDiscountUnit,
4409
4410
  productsForStep3 as productsForStep,
4410
4411
  reportAddToCart2 as reportAddToCart,
@@ -4416,7 +4417,6 @@ export {
4416
4417
  useAnalytics,
4417
4418
  useBundleData,
4418
4419
  useBundlesForProduct,
4419
- useShopSettings,
4420
4420
  useVariantSelection,
4421
4421
  useWidgetConfigVars,
4422
4422
  validateQuantity