@lime-bundles/react 4.1.0 → 6.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
@@ -20,57 +20,62 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- BUNDLES_FOR_PRODUCT_QUERY: () => import_core13.BUNDLES_FOR_PRODUCT_QUERY,
24
- BUNDLE_METAOBJECT_QUERY: () => import_core13.BUNDLE_METAOBJECT_QUERY,
25
- BundleParseError: () => import_core12.BundleParseError,
23
+ BUNDLES_FOR_PRODUCT_QUERY: () => import_core18.BUNDLES_FOR_PRODUCT_QUERY,
24
+ BUNDLE_METAOBJECT_QUERY: () => import_core18.BUNDLE_METAOBJECT_QUERY,
25
+ BogoBundle: () => BogoBundle,
26
+ BundleParseError: () => import_core17.BundleParseError,
27
+ DEFAULT_OUT_OF_STOCK_BEHAVIOR: () => import_core18.DEFAULT_OUT_OF_STOCK_BEHAVIOR,
26
28
  FixedBundle: () => FixedBundle,
27
29
  MixMatchBundle: () => MixMatchBundle,
28
- SHOP_CUSTOM_CSS_QUERY: () => import_core13.SHOP_CUSTOM_CSS_QUERY,
29
- StorefrontApiError: () => import_core12.StorefrontApiError,
30
+ MultiStepBundle: () => MultiStepBundle,
31
+ SHOP_SETTINGS_QUERY: () => import_core18.SHOP_SETTINGS_QUERY,
32
+ StorefrontApiError: () => import_core17.StorefrontApiError,
30
33
  VariantDropdown: () => VariantDropdown,
31
34
  VolumeBundle: () => VolumeBundle,
32
- WIDGET_CONFIG_DEFAULTS: () => import_core13.WIDGET_CONFIG_DEFAULTS,
33
- applyABVariantB: () => import_core13.applyABVariantB,
34
- applyWidgetConfigVars: () => import_core13.applyWidgetConfigVars,
35
- calculateDiscount: () => import_core13.calculateDiscount,
36
- calculateTierSavings: () => import_core13.calculateTierSavings,
37
- computeBundleSaleCents: () => import_core13.computeBundleSaleCents,
38
- computeFixedPricing: () => import_core13.computeFixedPricing,
39
- createStorefrontClient: () => import_core13.createStorefrontClient,
35
+ WIDGET_CONFIG_DEFAULTS: () => import_core18.WIDGET_CONFIG_DEFAULTS,
36
+ applyWidgetConfigVars: () => import_core18.applyWidgetConfigVars,
37
+ calculateDiscount: () => import_core18.calculateDiscount,
38
+ calculateTierSavings: () => import_core18.calculateTierSavings,
39
+ computeBundleSaleCents: () => import_core18.computeBundleSaleCents,
40
+ computeFixedPricing: () => import_core18.computeFixedPricing,
41
+ createStorefrontClient: () => import_core18.createStorefrontClient,
40
42
  fetchBundleData: () => fetchBundleData,
41
- fetchBundlesForProduct: () => import_core13.fetchBundlesForProduct,
43
+ fetchBundlesForProduct: () => import_core18.fetchBundlesForProduct,
42
44
  fetchShopCustomCss: () => fetchShopCustomCss,
43
- formatCents: () => import_core13.formatCents,
44
- formatCountdown: () => import_core13.formatCountdown,
45
- formatMoney: () => import_core13.formatMoney,
46
- formatUnitPrice: () => import_core13.formatUnitPrice,
47
- getABTestAssignment: () => import_core13.getABTestAssignment,
48
- getActiveTier: () => import_core13.getActiveTier,
49
- hasConsent: () => import_core13.hasConsent,
50
- injectCustomCss: () => import_core13.injectCustomCss,
51
- mergeWidgetConfig: () => import_core13.mergeWidgetConfig,
52
- observeImpression: () => import_core13.observeImpression,
53
- parseCents: () => import_core13.parseCents,
54
- parseMetaobjectBundle: () => import_core13.parseMetaobjectBundle,
55
- parseMetaobjectBundleStrict: () => import_core13.parseMetaobjectBundleStrict,
56
- percentageDiscountUnit: () => import_core13.percentageDiscountUnit,
57
- reportAddToCart: () => import_core13.reportAddToCart,
58
- reportImpression: () => import_core13.reportImpression,
59
- sanitizeCustomCss: () => import_core13.sanitizeCustomCss,
60
- setConsent: () => import_core13.setConsent,
61
- thumbnailRatioVars: () => import_core13.thumbnailRatioVars,
62
- transformImageUrl: () => import_core13.transformImageUrl,
45
+ fetchShopSettings: () => fetchShopSettings,
46
+ fetchShopSettingsOrDefault: () => fetchShopSettingsOrDefault,
47
+ formatCents: () => import_core18.formatCents,
48
+ formatCountdown: () => import_core18.formatCountdown,
49
+ formatMoney: () => import_core18.formatMoney,
50
+ formatUnitPrice: () => import_core18.formatUnitPrice,
51
+ getActiveTier: () => import_core18.getActiveTier,
52
+ injectCustomCss: () => import_core18.injectCustomCss,
53
+ mergeWidgetConfig: () => import_core18.mergeWidgetConfig,
54
+ observeImpression: () => import_core18.observeImpression,
55
+ parseCents: () => import_core18.parseCents,
56
+ parseMetaobjectBundle: () => import_core18.parseMetaobjectBundle,
57
+ parseMetaobjectBundleStrict: () => import_core18.parseMetaobjectBundleStrict,
58
+ parseOutOfStockBehavior: () => import_core18.parseOutOfStockBehavior,
59
+ percentageDiscountUnit: () => import_core18.percentageDiscountUnit,
60
+ productsForStep: () => import_core18.productsForStep,
61
+ reportAddToCart: () => import_core18.reportAddToCart,
62
+ reportImpression: () => import_core18.reportImpression,
63
+ sanitizeCustomCss: () => import_core18.sanitizeCustomCss,
64
+ stepHeadroom: () => import_core18.stepHeadroom,
65
+ thumbnailRatioVars: () => import_core18.thumbnailRatioVars,
66
+ transformImageUrl: () => import_core18.transformImageUrl,
63
67
  useAnalytics: () => useAnalytics,
64
68
  useBundleData: () => useBundleData,
65
69
  useBundlesForProduct: () => useBundlesForProduct,
70
+ useShopSettings: () => useShopSettings,
66
71
  useVariantSelection: () => useVariantSelection,
67
72
  useWidgetConfigVars: () => useWidgetConfigVars,
68
- validateQuantity: () => import_core13.validateQuantity
73
+ validateQuantity: () => import_core18.validateQuantity
69
74
  });
70
75
  module.exports = __toCommonJS(index_exports);
71
76
 
72
77
  // src/components/FixedBundle.tsx
73
- var import_react6 = require("react");
78
+ var import_react7 = require("react");
74
79
  var import_core8 = require("@lime-bundles/core");
75
80
 
76
81
  // src/hooks/useBundleData.ts
@@ -116,7 +121,7 @@ async function fetchBundleDataWithWarnings(options) {
116
121
  data.metaobject.id,
117
122
  data.metaobject.fields
118
123
  );
119
- if (bundle.marketVisibility === "specific" && (!options.marketId || !bundle.marketIds.includes(options.marketId))) {
124
+ if (!(0, import_core.isVisibleInMarket)(bundle, options.marketId)) {
120
125
  return {
121
126
  bundle: null,
122
127
  warnings: [
@@ -132,10 +137,14 @@ async function fetchBundleDataWithWarnings(options) {
132
137
  return { bundle, warnings: [] };
133
138
  }
134
139
 
135
- // src/fetchShopCustomCss.ts
140
+ // src/fetchShopSettings.ts
136
141
  var import_core2 = require("@lime-bundles/core");
142
+ var FALLBACK = {
143
+ customCss: null,
144
+ outOfStockBehavior: import_core2.DEFAULT_OUT_OF_STOCK_BEHAVIOR
145
+ };
137
146
  var cache = /* @__PURE__ */ new Map();
138
- async function fetchShopCustomCss(options) {
147
+ async function fetchShopSettings(options) {
139
148
  const cached = cache.get(options.shopDomain);
140
149
  if (cached) return cached;
141
150
  const promise = (async () => {
@@ -145,11 +154,16 @@ async function fetchShopCustomCss(options) {
145
154
  buyerIp: options.buyerIp
146
155
  });
147
156
  const data = await client.query(
148
- import_core2.SHOP_CUSTOM_CSS_QUERY,
157
+ import_core2.SHOP_SETTINGS_QUERY,
149
158
  void 0,
150
159
  { signal: options.signal }
151
160
  );
152
- return data.shop?.metafield?.value ?? null;
161
+ return {
162
+ customCss: data.shop?.customCss?.value ?? null,
163
+ outOfStockBehavior: (0, import_core2.parseOutOfStockBehavior)(
164
+ data.shop?.outOfStockBehavior?.value
165
+ )
166
+ };
153
167
  })();
154
168
  cache.set(options.shopDomain, promise);
155
169
  promise.catch(() => {
@@ -159,6 +173,13 @@ async function fetchShopCustomCss(options) {
159
173
  });
160
174
  return promise;
161
175
  }
176
+ async function fetchShopSettingsOrDefault(options) {
177
+ try {
178
+ return await fetchShopSettings(options);
179
+ } catch {
180
+ return FALLBACK;
181
+ }
182
+ }
162
183
 
163
184
  // src/hooks/useBundleData.ts
164
185
  var INITIAL_STATE = {
@@ -191,13 +212,13 @@ function useBundleData(options) {
191
212
  error: err instanceof Error ? err : new Error(String(err))
192
213
  });
193
214
  });
194
- fetchShopCustomCss({
215
+ fetchShopSettings({
195
216
  shopDomain: options.shopDomain,
196
217
  storefrontAccessToken: options.storefrontAccessToken,
197
218
  signal: controller.signal
198
- }).then((css) => {
219
+ }).then((settings) => {
199
220
  if (controller.signal.aborted) return;
200
- (0, import_core3.injectCustomCss)(options.shopDomain, css);
221
+ (0, import_core3.injectCustomCss)(options.shopDomain, settings.customCss);
201
222
  }).catch(() => {
202
223
  });
203
224
  return () => {
@@ -230,7 +251,7 @@ function useAnalytics(options) {
230
251
  );
231
252
  (0, import_react2.useEffect)(() => {
232
253
  impressionFiredRef.current = false;
233
- }, [options.bundleGid, options.abVariant]);
254
+ }, [options.bundleGid]);
234
255
  (0, import_react2.useEffect)(() => {
235
256
  if (options.enabled === false) return;
236
257
  if (impressionFiredRef.current) return;
@@ -240,9 +261,7 @@ function useAnalytics(options) {
240
261
  impressionFiredRef.current = true;
241
262
  (0, import_core4.reportImpression)(config, {
242
263
  bundleGid: options.bundleGid,
243
- bundleType: options.bundleType,
244
- abTestId: options.abTestId,
245
- abVariant: options.abVariant
264
+ bundleType: options.bundleType
246
265
  });
247
266
  });
248
267
  return cleanup;
@@ -251,9 +270,7 @@ function useAnalytics(options) {
251
270
  config,
252
271
  options.enabled,
253
272
  options.bundleGid,
254
- options.bundleType,
255
- options.abTestId,
256
- options.abVariant
273
+ options.bundleType
257
274
  ]);
258
275
  const trackAddToCart = (0, import_react2.useCallback)(
259
276
  (event) => {
@@ -263,18 +280,14 @@ function useAnalytics(options) {
263
280
  bundleType: options.bundleType,
264
281
  productId: event.productId,
265
282
  quantity: event.quantity,
266
- totalPrice: event.totalPrice,
267
- abTestId: options.abTestId,
268
- abVariant: options.abVariant
283
+ totalPrice: event.totalPrice
269
284
  });
270
285
  },
271
286
  [
272
287
  config,
273
288
  options.enabled,
274
289
  options.bundleGid,
275
- options.bundleType,
276
- options.abTestId,
277
- options.abVariant
290
+ options.bundleType
278
291
  ]
279
292
  );
280
293
  return { elementRef, trackAddToCart };
@@ -378,8 +391,53 @@ function useVariantSelection({
378
391
  };
379
392
  }
380
393
 
381
- // src/components/VariantDropdown.tsx
394
+ // src/hooks/useEdgeFade.ts
382
395
  var import_react5 = require("react");
396
+ function attachEdgeFade(el) {
397
+ function update() {
398
+ const scrollable = el.scrollHeight - el.clientHeight;
399
+ if (scrollable <= 1) {
400
+ el.removeAttribute("data-edge-fade");
401
+ return;
402
+ }
403
+ const atTop = el.scrollTop <= 1;
404
+ const atBottom = el.scrollTop >= scrollable - 1;
405
+ el.setAttribute(
406
+ "data-edge-fade",
407
+ atTop ? "bottom" : atBottom ? "top" : "both"
408
+ );
409
+ }
410
+ el.addEventListener("scroll", update, { passive: true });
411
+ let resizeObserver = null;
412
+ if (typeof ResizeObserver !== "undefined") {
413
+ resizeObserver = new ResizeObserver(update);
414
+ resizeObserver.observe(el);
415
+ }
416
+ let mutationObserver = null;
417
+ if (typeof MutationObserver !== "undefined") {
418
+ mutationObserver = new MutationObserver(update);
419
+ mutationObserver.observe(el, { childList: true, subtree: true });
420
+ }
421
+ update();
422
+ return () => {
423
+ el.removeEventListener("scroll", update);
424
+ resizeObserver?.disconnect();
425
+ mutationObserver?.disconnect();
426
+ };
427
+ }
428
+ function useEdgeFade() {
429
+ const cleanupRef = (0, import_react5.useRef)(null);
430
+ return (0, import_react5.useCallback)((el) => {
431
+ cleanupRef.current?.();
432
+ cleanupRef.current = null;
433
+ if (el) {
434
+ cleanupRef.current = attachEdgeFade(el);
435
+ }
436
+ }, []);
437
+ }
438
+
439
+ // src/components/VariantDropdown.tsx
440
+ var import_react6 = require("react");
383
441
  var import_core7 = require("@lime-bundles/core");
384
442
  var import_jsx_runtime = require("react/jsx-runtime");
385
443
  var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = import_core7.dropdown;
@@ -407,18 +465,18 @@ function VariantDropdown({
407
465
  ariaLabel,
408
466
  className
409
467
  }) {
410
- const idBase = (0, import_react5.useId)();
411
- const triggerRef = (0, import_react5.useRef)(null);
412
- const listboxRef = (0, import_react5.useRef)(null);
413
- const typeAheadRef = (0, import_react5.useRef)(emptyTypeAheadState());
414
- const [isOpen, setIsOpen] = (0, import_react5.useState)(false);
415
- const [activeIndex, setActiveIndex] = (0, import_react5.useState)(-1);
416
- const [position, setPosition] = (0, import_react5.useState)(null);
417
- const selectedIndex = (0, import_react5.useMemo)(
468
+ const idBase = (0, import_react6.useId)();
469
+ const triggerRef = (0, import_react6.useRef)(null);
470
+ const listboxRef = (0, import_react6.useRef)(null);
471
+ const typeAheadRef = (0, import_react6.useRef)(emptyTypeAheadState());
472
+ const [isOpen, setIsOpen] = (0, import_react6.useState)(false);
473
+ const [activeIndex, setActiveIndex] = (0, import_react6.useState)(-1);
474
+ const [position, setPosition] = (0, import_react6.useState)(null);
475
+ const selectedIndex = (0, import_react6.useMemo)(
418
476
  () => options.findIndex((o) => o.value === value),
419
477
  [options, value]
420
478
  );
421
- const optionsState = (0, import_react5.useMemo)(
479
+ const optionsState = (0, import_react6.useMemo)(
422
480
  () => options.map((o) => ({ disabled: !!o.disabled, label: o.label })),
423
481
  [options]
424
482
  );
@@ -441,7 +499,7 @@ function VariantDropdown({
441
499
  if (opt.value !== value) onChange(opt.value);
442
500
  close(true);
443
501
  }
444
- (0, import_react5.useLayoutEffect)(() => {
502
+ (0, import_react6.useLayoutEffect)(() => {
445
503
  if (!isOpen) return;
446
504
  const trigger = triggerRef.current;
447
505
  if (!trigger) return;
@@ -468,7 +526,7 @@ function VariantDropdown({
468
526
  })
469
527
  );
470
528
  }, [isOpen, options.length]);
471
- (0, import_react5.useEffect)(() => {
529
+ (0, import_react6.useEffect)(() => {
472
530
  if (!isOpen) return;
473
531
  const isInsideDropdown = (event) => {
474
532
  const path = event.composedPath();
@@ -494,7 +552,7 @@ function VariantDropdown({
494
552
  window.removeEventListener("resize", onResize);
495
553
  };
496
554
  }, [isOpen]);
497
- (0, import_react5.useEffect)(() => {
555
+ (0, import_react6.useEffect)(() => {
498
556
  if (!isOpen || activeIndex < 0) return;
499
557
  const listbox = listboxRef.current;
500
558
  if (!listbox) return;
@@ -669,10 +727,10 @@ function FixedBundle(props) {
669
727
  bundleType: "fixed",
670
728
  enabled: analyticsEnabled !== false
671
729
  });
672
- const [addingToCart, setAddingToCart] = (0, import_react6.useState)(false);
673
- const [cartError, setCartError] = (0, import_react6.useState)(null);
674
- const [selectedVariants, setSelectedVariants] = (0, import_react6.useState)({});
675
- const handleVariantChange = (0, import_react6.useCallback)(
730
+ const [addingToCart, setAddingToCart] = (0, import_react7.useState)(false);
731
+ const [cartError, setCartError] = (0, import_react7.useState)(null);
732
+ const [selectedVariants, setSelectedVariants] = (0, import_react7.useState)({});
733
+ const handleVariantChange = (0, import_react7.useCallback)(
676
734
  (productId, variant) => {
677
735
  setSelectedVariants((prev) => {
678
736
  if (prev[productId] === variant) return prev;
@@ -683,7 +741,8 @@ function FixedBundle(props) {
683
741
  );
684
742
  const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
685
743
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
686
- (0, import_react6.useEffect)(() => {
744
+ const productsEdgeFadeRef = useEdgeFade();
745
+ (0, import_react7.useEffect)(() => {
687
746
  if (result.status === "error") {
688
747
  onError?.(result.error);
689
748
  return;
@@ -696,7 +755,17 @@ function FixedBundle(props) {
696
755
  );
697
756
  }
698
757
  }, [result, onError]);
699
- const handleAddToCart = (0, import_react6.useCallback)(async () => {
758
+ const oosProductIds = (0, import_react7.useMemo)(() => {
759
+ if (!bundle) return /* @__PURE__ */ new Set();
760
+ return new Set(
761
+ bundle.products.filter(
762
+ (p) => !p.variants.nodes.some(
763
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, p.id, v.id))
764
+ )
765
+ ).map((p) => p.id)
766
+ );
767
+ }, [bundle]);
768
+ const handleAddToCart = (0, import_react7.useCallback)(async () => {
700
769
  if (!bundle) return;
701
770
  const bundleLines = bundle.products.filter(
702
771
  (p) => p.variants.nodes.some(
@@ -759,6 +828,27 @@ function FixedBundle(props) {
759
828
  if (result.status === "error") return null;
760
829
  if (!bundle) return null;
761
830
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
831
+ let comparePrice = 0;
832
+ let salePrice = 0;
833
+ for (const product of bundle.products) {
834
+ const variants = product.variants.nodes;
835
+ const variant = selectedVariants[product.id] ?? variants.find(
836
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, product.id, v.id))
837
+ ) ?? variants[0];
838
+ if (!variant) continue;
839
+ const qty = (0, import_core8.resolveBundleQty)(bundle, product.id, variant.id);
840
+ const unit = parseFloat(variant.price.amount);
841
+ comparePrice += unit * qty;
842
+ salePrice += (0, import_core8.calculateDiscount)(
843
+ unit,
844
+ bundle.discountConfig.discountType,
845
+ bundle.discountConfig.discountValue
846
+ ) * qty;
847
+ }
848
+ const savings = Math.max(0, comparePrice - salePrice);
849
+ const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
850
+ const showCompare = bundle.widgetConfig.pricing.showComparePrice && savings > 0;
851
+ const showSavings = bundle.widgetConfig.savingsBar.visible && savings > 0;
762
852
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
763
853
  "div",
764
854
  {
@@ -770,36 +860,60 @@ function FixedBundle(props) {
770
860
  role: "region",
771
861
  "aria-label": bundle.title,
772
862
  children: [
773
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
774
- bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
775
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
863
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle-header", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle-header__content", children: [
864
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle-title", children: bundle.title }),
865
+ bundle.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle-subtitle", children: bundle.description })
866
+ ] }) }),
867
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products lb-edge-fade", ref: productsEdgeFadeRef, children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
776
868
  FixedProductRow,
777
869
  {
778
870
  bundle,
779
871
  product,
780
872
  currency,
873
+ isOos: oosProductIds.has(product.id),
781
874
  onVariantChange: handleVariantChange
782
875
  },
783
876
  product.id
784
877
  )) }),
878
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle-divider" }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle-summary", children: [
880
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle-summary__text", children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle-summary__label", children: "Bundle price" }),
882
+ showSavings && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
883
+ "You save",
884
+ " ",
885
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "data-savings-amount": true, children: (0, import_core8.formatMoney)(savings, currency) }),
886
+ " ",
887
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { "data-savings-percent": true, children: [
888
+ "(",
889
+ savingsPercent,
890
+ "%)"
891
+ ] })
892
+ ] })
893
+ ] }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-summary__prices", children: [
895
+ showCompare && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: (0, import_core8.formatMoney)(comparePrice, currency) }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: (0, import_core8.formatMoney)(salePrice, currency) })
897
+ ] })
898
+ ] }),
785
899
  cartError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
786
900
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
787
901
  "button",
788
902
  {
789
903
  className: "lb-bundle__cta",
790
904
  onClick: handleAddToCart,
791
- disabled: addingToCart,
905
+ disabled: addingToCart || oosProductIds.size > 0,
792
906
  "aria-busy": addingToCart,
793
- children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
907
+ children: addingToCart ? "Adding..." : oosProductIds.size > 0 ? `${oosProductIds.size} item${oosProductIds.size === 1 ? "" : "s"} unavailable` : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
794
908
  }
795
909
  )
796
910
  ]
797
911
  }
798
912
  );
799
913
  }
800
- function FixedProductRow({ bundle, product, currency, onVariantChange }) {
914
+ function FixedProductRow({ bundle, product, currency, isOos, onVariantChange }) {
801
915
  const variants = product.variants.nodes;
802
- const optionNames = (0, import_react6.useMemo)(() => {
916
+ const optionNames = (0, import_react7.useMemo)(() => {
803
917
  const first = variants[0];
804
918
  return first ? first.selectedOptions.map((o) => o.name) : [];
805
919
  }, [variants]);
@@ -808,18 +922,20 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
808
922
  variants,
809
923
  optionNames
810
924
  });
811
- (0, import_react6.useEffect)(() => {
925
+ (0, import_react7.useEffect)(() => {
812
926
  onVariantChange(product.id, selectedVariant ?? null);
813
927
  }, [selectedVariant, product.id, onVariantChange]);
814
928
  const displayVariant = selectedVariant ?? variants.find(
815
929
  (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, product.id, v.id))
816
930
  ) ?? variants[0];
817
931
  const priceText = displayVariant ? (0, import_core8.formatMoney)(displayVariant.price.amount, currency) : (0, import_core8.formatMoney)(product.priceRange.minVariantPrice.amount, currency);
932
+ const compareAt = displayVariant?.compareAtPrice && parseFloat(displayVariant.compareAtPrice.amount) > parseFloat(displayVariant.price.amount) ? (0, import_core8.formatMoney)(displayVariant.compareAtPrice.amount, currency) : null;
818
933
  const unitPriceText = displayVariant ? (0, import_core8.formatUnitPrice)(
819
934
  displayVariant.unitPrice,
820
935
  displayVariant.unitPriceMeasurement,
821
936
  currency
822
937
  ) : null;
938
+ const displayQty = displayVariant ? (0, import_core8.resolveBundleQty)(bundle, product.id, displayVariant.id) : 1;
823
939
  const thumbImage = displayVariant?.image ?? product.featuredImage;
824
940
  const showLowStock = !!displayVariant && (0, import_core8.shouldShowLowStockBadge)(
825
941
  displayVariant,
@@ -827,297 +943,15 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
827
943
  bundle.widgetConfig.lowStockThreshold,
828
944
  bundle.widgetConfig.showLowStockBadge
829
945
  );
830
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
831
- thumbImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
832
- "img",
833
- {
834
- src: thumbImage.url,
835
- alt: thumbImage.altText ?? product.title,
836
- className: "lb-bundle__product-image",
837
- loading: "lazy"
838
- }
839
- ),
840
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
841
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
842
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: priceText }),
843
- unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
844
- showLowStock && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
845
- "Only ",
846
- displayVariant.quantityAvailable,
847
- " left"
848
- ] }),
849
- showPicker && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
850
- const dropdownOptions = optionsFor(optionIndex).map((o) => ({
851
- value: o.value,
852
- label: o.value,
853
- disabled: o.disabled
854
- }));
855
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
856
- VariantDropdown,
857
- {
858
- options: dropdownOptions,
859
- value: selectedValues[optionIndex] ?? null,
860
- onChange: (v) => setOptionValue(optionIndex, v),
861
- ariaLabel: optionName
862
- },
863
- optionName
864
- );
865
- }) })
866
- ] })
867
- ] });
868
- }
869
-
870
- // src/components/MixMatchBundle.tsx
871
- var import_react7 = require("react");
872
- var import_core9 = require("@lime-bundles/core");
873
- var import_jsx_runtime3 = require("react/jsx-runtime");
874
- function ruleFor(bundle, productId) {
875
- return bundle.productRules[productId] ?? import_core9.DEFAULT_PRODUCT_RULE;
876
- }
877
- function MixMatchBundle(props) {
878
- const {
879
- shopDomain,
880
- storefrontAccessToken,
881
- bundleGid,
882
- appUrl,
883
- analyticsEnabled,
884
- onAddToCart,
885
- onError,
886
- className
887
- } = props;
888
- const result = useBundleData({
889
- shopDomain,
890
- storefrontAccessToken,
891
- bundleGid
892
- });
893
- const { elementRef, trackAddToCart } = useAnalytics({
894
- shopDomain,
895
- appUrl: appUrl ?? `https://${shopDomain}`,
896
- bundleGid,
897
- bundleType: "mix_match",
898
- enabled: analyticsEnabled !== false
899
- });
900
- const [selections, setSelections] = (0, import_react7.useState)(
901
- /* @__PURE__ */ new Map()
902
- );
903
- const [addingToCart, setAddingToCart] = (0, import_react7.useState)(false);
904
- const [cartError, setCartError] = (0, import_react7.useState)(null);
905
- const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
906
- const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
907
- (0, import_react7.useEffect)(() => {
908
- if (result.status === "error") {
909
- onError?.(result.error);
910
- return;
911
- }
912
- if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
913
- onError?.(
914
- new Error(
915
- `MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
916
- )
917
- );
918
- }
919
- }, [result, onError]);
920
- const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
921
- const distinctSelectedProducts = (0, import_react7.useMemo)(() => {
922
- const ids = /* @__PURE__ */ new Set();
923
- for (const sel of selections.values()) {
924
- if (sel.quantity > 0) ids.add(sel.productId);
925
- }
926
- return ids;
927
- }, [selections]);
928
- const totalQuantity = (0, import_react7.useMemo)(
929
- () => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
930
- [selections]
931
- );
932
- const requiredPicks = bundle?.minQuantity ?? 0;
933
- const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
934
- const valid = !!bundle && meetsMinPicks;
935
- const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
936
- const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
937
- const selectVariant = (0, import_react7.useCallback)(
938
- (productId, variant) => {
939
- if (!bundle) return;
940
- const rule = ruleFor(bundle, productId);
941
- const maxAddable = (0, import_core9.maxAddableQuantity)(variant, rule.max, 0);
942
- if (maxAddable < rule.min) return;
943
- setSelections((prev) => {
944
- const next = new Map(prev);
945
- next.set(`${productId}:${variant.id}`, {
946
- productId,
947
- variantId: variant.id,
948
- quantity: rule.min
949
- });
950
- return next;
951
- });
952
- },
953
- [bundle]
954
- );
955
- const deselect = (0, import_react7.useCallback)((productId, variantId) => {
956
- setSelections((prev) => {
957
- if (!prev.has(`${productId}:${variantId}`)) return prev;
958
- const next = new Map(prev);
959
- next.delete(`${productId}:${variantId}`);
960
- return next;
961
- });
962
- }, []);
963
- const setQuantity = (0, import_react7.useCallback)(
964
- (productId, variant, quantity) => {
965
- if (!bundle) return;
966
- const rule = ruleFor(bundle, productId);
967
- const maxAddable = (0, import_core9.maxAddableQuantity)(variant, rule.max, 0);
968
- const key = `${productId}:${variant.id}`;
969
- setSelections((prev) => {
970
- const next = new Map(prev);
971
- if (quantity < rule.min || maxAddable < rule.min) {
972
- next.delete(key);
973
- return next;
974
- }
975
- const clamped = Math.min(quantity, maxAddable);
976
- next.set(key, {
977
- productId,
978
- variantId: variant.id,
979
- quantity: clamped
980
- });
981
- return next;
982
- });
983
- },
984
- [bundle]
985
- );
986
- const handleAddToCart = (0, import_react7.useCallback)(async () => {
987
- if (!bundle || !valid) return;
988
- const byVariant = /* @__PURE__ */ new Map();
989
- for (const sel of selections.values()) {
990
- if (sel.quantity <= 0) continue;
991
- byVariant.set(
992
- sel.variantId,
993
- (byVariant.get(sel.variantId) ?? 0) + sel.quantity
994
- );
995
- }
996
- const lines = Array.from(byVariant.entries()).map(
997
- ([variantId, quantity]) => ({
998
- merchandiseId: variantId,
999
- quantity,
1000
- attributes: [
1001
- { key: "_lime_bundle_gid", value: bundle.id },
1002
- { key: "_lime_bundle_type", value: bundle.bundleType }
1003
- ]
1004
- })
1005
- );
1006
- setAddingToCart(true);
1007
- setCartError(null);
1008
- try {
1009
- await onAddToCart(lines);
1010
- const totalPrice = lines.reduce((sum, line) => {
1011
- const product = bundle.products.find(
1012
- (p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
1013
- );
1014
- const variant = product?.variants.nodes.find(
1015
- (v) => v.id === line.merchandiseId
1016
- );
1017
- return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
1018
- }, 0);
1019
- trackAddToCart({
1020
- quantity: totalQuantity,
1021
- totalPrice: Math.round(totalPrice * 100) / 100
1022
- });
1023
- setSelections(/* @__PURE__ */ new Map());
1024
- } catch (err) {
1025
- const error = err instanceof Error ? err : new Error(String(err));
1026
- setCartError(error.message);
1027
- onError?.(error);
1028
- } finally {
1029
- setAddingToCart(false);
1030
- }
1031
- }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
1032
- if (result.status === "loading") {
1033
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1034
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
1035
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
1036
- ] });
1037
- }
1038
- if (result.status === "error") return null;
1039
- if (!bundle) return null;
1040
- const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
1041
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1042
- "div",
1043
- {
1044
- ref: (el) => {
1045
- elementRef(el);
1046
- setConfigVarsRef(el);
1047
- },
1048
- className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
1049
- role: "region",
1050
- "aria-label": bundle.title,
1051
- children: [
1052
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
1053
- bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
1054
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
1055
- /* @__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)(
1056
- MixMatchProductRow,
1057
- {
1058
- bundle,
1059
- product,
1060
- currency,
1061
- showStepper,
1062
- selections,
1063
- onSelect: selectVariant,
1064
- onDeselect: deselect,
1065
- onSetQuantity: setQuantity
1066
- },
1067
- product.id
1068
- )) }),
1069
- validationMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
1070
- cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1071
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1072
- "button",
1073
- {
1074
- className: "lb-bundle__cta",
1075
- onClick: handleAddToCart,
1076
- disabled: addingToCart || !valid,
1077
- "aria-busy": addingToCart,
1078
- children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
1079
- }
1080
- )
1081
- ]
1082
- }
1083
- );
1084
- }
1085
- function MixMatchProductRow({
1086
- bundle,
1087
- product,
1088
- currency,
1089
- showStepper,
1090
- selections,
1091
- onSelect,
1092
- onDeselect,
1093
- onSetQuantity
1094
- }) {
1095
- const variants = product.variants.nodes;
1096
- const optionNames = (0, import_react7.useMemo)(() => {
1097
- const first = variants[0];
1098
- return first ? first.selectedOptions.map((o) => o.name) : [];
1099
- }, [variants]);
1100
- const showPicker = variants.length > 1 && optionNames.length > 0;
1101
- const rule = ruleFor(bundle, product.id);
1102
- const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1103
- const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core9.isVariantFulfillable)(v, rule.min)) ?? variants[0] ?? null;
1104
- if (!displayVariant) return null;
1105
- const fulfillable = (0, import_core9.isVariantFulfillable)(displayVariant, rule.min);
1106
- const maxAddable = (0, import_core9.maxAddableQuantity)(displayVariant, rule.max, 0);
1107
- const key = `${product.id}:${displayVariant.id}`;
1108
- const selected = selections.get(key);
1109
- const thumbImage = displayVariant.image ?? product.featuredImage;
1110
- const unitPriceText = (0, import_core9.formatUnitPrice)(
1111
- displayVariant.unitPrice,
1112
- displayVariant.unitPriceMeasurement,
1113
- currency
1114
- );
1115
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
946
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1116
947
  "div",
1117
948
  {
1118
- className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
949
+ className: isOos ? "lb-bundle-product-row lb-bundle-product-row--oos" : "lb-bundle-product-row",
950
+ "aria-disabled": isOos || void 0,
951
+ part: "product",
1119
952
  children: [
1120
- thumbImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
953
+ isOos && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle-product-row__oos", children: "Out of stock" }),
954
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1121
955
  "img",
1122
956
  {
1123
957
  src: thumbImage.url,
@@ -1126,27 +960,40 @@ function MixMatchProductRow({
1126
960
  loading: "lazy"
1127
961
  }
1128
962
  ),
1129
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
1130
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1131
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core9.formatMoney)(displayVariant.price.amount, currency) }),
1132
- unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1133
- (0, import_core9.shouldShowLowStockBadge)(
1134
- displayVariant,
1135
- rule.min,
1136
- bundle.widgetConfig.lowStockThreshold,
1137
- bundle.widgetConfig.showLowStockBadge
1138
- ) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
963
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
964
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
965
+ !showPicker && displayVariant && variants.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle-variant-badge", children: displayVariant.title }),
966
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-product-prices", children: [
967
+ compareAt && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
968
+ "span",
969
+ {
970
+ className: "lb-bundle-product-compare-price",
971
+ "data-product-compare-price": true,
972
+ children: compareAt
973
+ }
974
+ ),
975
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__product-price", "data-product-price": true, children: priceText })
976
+ ] }),
977
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
978
+ "span",
979
+ {
980
+ className: "lb-bundle__product-unit-price",
981
+ "data-product-unit-price": true,
982
+ children: unitPriceText
983
+ }
984
+ ),
985
+ showLowStock && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1139
986
  "Only ",
1140
987
  displayVariant.quantityAvailable,
1141
988
  " left"
1142
989
  ] }),
1143
- showPicker && !selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
990
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1144
991
  const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1145
992
  value: o.value,
1146
993
  label: o.value,
1147
994
  disabled: o.disabled
1148
995
  }));
1149
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
996
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1150
997
  VariantDropdown,
1151
998
  {
1152
999
  options: dropdownOptions,
@@ -1158,71 +1005,2543 @@ function MixMatchProductRow({
1158
1005
  );
1159
1006
  }) })
1160
1007
  ] }),
1161
- /* @__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: [
1162
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1163
- "button",
1164
- {
1165
- "aria-label": `Decrease ${product.title}`,
1166
- onClick: () => onSetQuantity(
1167
- product.id,
1168
- displayVariant,
1169
- selected.quantity - 1
1170
- ),
1171
- disabled: selected.quantity <= rule.min,
1172
- children: "\u2212"
1173
- }
1174
- ),
1175
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-live": "polite", children: selected.quantity }),
1176
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1177
- "button",
1178
- {
1179
- "aria-label": `Increase ${product.title}`,
1180
- onClick: () => onSetQuantity(
1181
- product.id,
1182
- displayVariant,
1183
- selected.quantity + 1
1184
- ),
1185
- disabled: selected.quantity >= maxAddable,
1186
- children: "+"
1187
- }
1188
- ),
1189
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1190
- "button",
1191
- {
1192
- className: "lb-bundle__remove-btn",
1193
- "aria-label": `Remove ${product.title}`,
1194
- onClick: () => onDeselect(product.id, displayVariant.id),
1195
- children: "Remove"
1196
- }
1197
- )
1198
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1199
- "button",
1200
- {
1201
- className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
1202
- "aria-label": `Remove ${product.title}`,
1203
- onClick: () => onDeselect(product.id, displayVariant.id),
1204
- children: "Selected"
1205
- }
1206
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1207
- "button",
1208
- {
1209
- className: "lb-bundle__select-btn",
1210
- onClick: () => onSelect(product.id, displayVariant),
1211
- disabled: !fulfillable || maxAddable <= 0,
1212
- children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
1213
- }
1214
- ) })
1008
+ displayVariant && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-qty-chip", "data-qty-inline": true, children: [
1009
+ "\xD7",
1010
+ displayQty
1011
+ ] })
1215
1012
  ]
1216
1013
  }
1217
1014
  );
1218
1015
  }
1219
1016
 
1220
- // src/components/VolumeBundle.tsx
1017
+ // src/components/MixMatchBundle.tsx
1018
+ var import_react12 = require("react");
1019
+ var import_core11 = require("@lime-bundles/core");
1020
+
1021
+ // src/hooks/useShopSettings.ts
1221
1022
  var import_react8 = require("react");
1222
- var import_core10 = require("@lime-bundles/core");
1223
- var import_jsx_runtime4 = require("react/jsx-runtime");
1224
- function VolumeBundle(props) {
1225
- const {
1023
+ var import_core9 = require("@lime-bundles/core");
1024
+ function useShopSettings(options) {
1025
+ const [outOfStockBehavior, setOutOfStockBehavior] = (0, import_react8.useState)(import_core9.DEFAULT_OUT_OF_STOCK_BEHAVIOR);
1026
+ const { shopDomain, storefrontAccessToken } = options;
1027
+ (0, import_react8.useEffect)(() => {
1028
+ const controller = new AbortController();
1029
+ let active = true;
1030
+ fetchShopSettingsOrDefault({
1031
+ shopDomain,
1032
+ storefrontAccessToken,
1033
+ signal: controller.signal
1034
+ }).then((settings) => {
1035
+ if (!active || controller.signal.aborted) return;
1036
+ setOutOfStockBehavior(settings.outOfStockBehavior);
1037
+ });
1038
+ return () => {
1039
+ active = false;
1040
+ controller.abort();
1041
+ };
1042
+ }, [shopDomain, storefrontAccessToken]);
1043
+ return { outOfStockBehavior };
1044
+ }
1045
+
1046
+ // src/components/MixMatchPicker.tsx
1047
+ var import_react11 = require("react");
1048
+ var import_react_dom = require("react-dom");
1049
+ var import_core10 = require("@lime-bundles/core");
1050
+
1051
+ // src/hooks/useScrollLock.ts
1052
+ var import_react9 = require("react");
1053
+ var lockCount = 0;
1054
+ var savedY = 0;
1055
+ function lock() {
1056
+ if (typeof document === "undefined") return;
1057
+ lockCount += 1;
1058
+ if (lockCount > 1) return;
1059
+ savedY = window.pageYOffset || document.documentElement.scrollTop;
1060
+ const body = document.body;
1061
+ body.style.position = "fixed";
1062
+ body.style.top = `-${savedY}px`;
1063
+ body.style.left = "0";
1064
+ body.style.right = "0";
1065
+ body.style.overflow = "hidden";
1066
+ }
1067
+ function unlock() {
1068
+ if (typeof document === "undefined") return;
1069
+ if (lockCount <= 0) return;
1070
+ lockCount -= 1;
1071
+ if (lockCount > 0) return;
1072
+ const body = document.body;
1073
+ body.style.position = "";
1074
+ body.style.top = "";
1075
+ body.style.left = "";
1076
+ body.style.right = "";
1077
+ body.style.overflow = "";
1078
+ window.scrollTo(0, savedY);
1079
+ }
1080
+ function useScrollLock(active) {
1081
+ (0, import_react9.useEffect)(() => {
1082
+ if (!active) return;
1083
+ lock();
1084
+ return unlock;
1085
+ }, [active]);
1086
+ }
1087
+
1088
+ // src/hooks/useFocusTrap.ts
1089
+ var import_react10 = require("react");
1090
+ var FOCUSABLE_SELECTOR = [
1091
+ "a[href]",
1092
+ "button:not([disabled])",
1093
+ "input:not([disabled])",
1094
+ "select:not([disabled])",
1095
+ "textarea:not([disabled])",
1096
+ '[tabindex]:not([tabindex="-1"])'
1097
+ ].join(",");
1098
+ function getFocusable(container) {
1099
+ const nodes = container.querySelectorAll(FOCUSABLE_SELECTOR);
1100
+ const result = [];
1101
+ nodes.forEach((node) => {
1102
+ if (node.offsetParent !== null || typeof node.offsetParent === "undefined") {
1103
+ result.push(node);
1104
+ }
1105
+ });
1106
+ return result;
1107
+ }
1108
+ function useFocusTrap({
1109
+ active,
1110
+ containerRef,
1111
+ onEscape,
1112
+ returnFocusRef
1113
+ }) {
1114
+ const previouslyFocused = (0, import_react10.useRef)(null);
1115
+ (0, import_react10.useEffect)(() => {
1116
+ if (!active) return;
1117
+ const container = containerRef.current;
1118
+ previouslyFocused.current = document.activeElement ?? null;
1119
+ container?.focus();
1120
+ function onKeyDown(e) {
1121
+ if (e.key === "Escape") {
1122
+ e.preventDefault();
1123
+ onEscape();
1124
+ return;
1125
+ }
1126
+ if (e.key !== "Tab") return;
1127
+ const node = containerRef.current;
1128
+ if (!node) return;
1129
+ const focusable = getFocusable(node);
1130
+ if (focusable.length === 0) {
1131
+ e.preventDefault();
1132
+ return;
1133
+ }
1134
+ const first = focusable[0];
1135
+ const last = focusable[focusable.length - 1];
1136
+ const activeEl = document.activeElement;
1137
+ if (e.shiftKey && activeEl === first) {
1138
+ e.preventDefault();
1139
+ last.focus();
1140
+ } else if (!e.shiftKey && activeEl === last) {
1141
+ e.preventDefault();
1142
+ first.focus();
1143
+ }
1144
+ }
1145
+ document.addEventListener("keydown", onKeyDown);
1146
+ return () => {
1147
+ document.removeEventListener("keydown", onKeyDown);
1148
+ const target = returnFocusRef?.current ?? previouslyFocused.current;
1149
+ if (target && typeof target.focus === "function") {
1150
+ target.focus();
1151
+ }
1152
+ };
1153
+ }, [active]);
1154
+ }
1155
+
1156
+ // src/components/MixMatchPicker.tsx
1157
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1158
+ function ruleFor(bundle, productId) {
1159
+ return bundle.productRules[productId] ?? import_core10.DEFAULT_PRODUCT_RULE;
1160
+ }
1161
+ function sanitizeId(gid) {
1162
+ return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
1163
+ }
1164
+ function buildEligibleProducts(bundle, oosBehavior) {
1165
+ const result = [];
1166
+ const seen = /* @__PURE__ */ new Set();
1167
+ for (const product of bundle.products) {
1168
+ if (seen.has(product.id)) continue;
1169
+ seen.add(product.id);
1170
+ const rule = ruleFor(bundle, product.id);
1171
+ const available = product.variants.nodes.filter(
1172
+ (v) => (0, import_core10.isVariantFulfillable)(v, rule.min)
1173
+ );
1174
+ const isOos = available.length === 0;
1175
+ if (isOos && oosBehavior === "hide") continue;
1176
+ result.push({
1177
+ product,
1178
+ variants: product.variants.nodes,
1179
+ firstAvailableVariant: available[0] ?? null,
1180
+ isOos
1181
+ });
1182
+ }
1183
+ return result;
1184
+ }
1185
+ function normalizeText(str) {
1186
+ return str.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
1187
+ }
1188
+ var FILTERS_ALL = "__all__";
1189
+ function MixMatchPicker({
1190
+ outOfStockBehavior,
1191
+ bundle,
1192
+ currency,
1193
+ requiredQty,
1194
+ showStepper,
1195
+ selections,
1196
+ onAdd,
1197
+ onUpdateQuantity,
1198
+ onRemoveVariant,
1199
+ canRemoveVariant,
1200
+ swapUnitsFor,
1201
+ onSwap,
1202
+ onClose,
1203
+ styleVarsRef
1204
+ }) {
1205
+ const wc = bundle.widgetConfig;
1206
+ const overlayRef = (0, import_react11.useRef)(null);
1207
+ const dialogRef = (0, import_react11.useRef)(null);
1208
+ const mouseDownTarget = (0, import_react11.useRef)(null);
1209
+ const [open, setOpen] = (0, import_react11.useState)(false);
1210
+ const [query, setQuery] = (0, import_react11.useState)("");
1211
+ const [activeType, setActiveType] = (0, import_react11.useState)(FILTERS_ALL);
1212
+ const eligible = (0, import_react11.useMemo)(
1213
+ () => buildEligibleProducts(bundle, outOfStockBehavior),
1214
+ [bundle, outOfStockBehavior]
1215
+ );
1216
+ const productTypes = (0, import_react11.useMemo)(() => {
1217
+ const types = [];
1218
+ const seen = /* @__PURE__ */ new Set();
1219
+ for (const ep of eligible) {
1220
+ const ty = (ep.product.productType ?? "").trim();
1221
+ if (ty && !seen.has(ty)) {
1222
+ seen.add(ty);
1223
+ types.push(ty);
1224
+ }
1225
+ }
1226
+ return types;
1227
+ }, [eligible]);
1228
+ const showFilters = wc.mixMatchShowTypeFilters && productTypes.length >= 2;
1229
+ const totalUnits = selections.reduce((sum, s) => sum + s.quantity, 0);
1230
+ const atCapacity = totalUnits >= requiredQty;
1231
+ const shownUnits = Math.min(totalUnits, requiredQty);
1232
+ (0, import_react11.useEffect)(() => {
1233
+ const id = requestAnimationFrame(() => setOpen(true));
1234
+ return () => cancelAnimationFrame(id);
1235
+ }, []);
1236
+ useScrollLock(true);
1237
+ useFocusTrap({
1238
+ active: true,
1239
+ containerRef: dialogRef,
1240
+ onEscape: onClose
1241
+ });
1242
+ const remaining = Math.max(0, requiredQty - shownUnits);
1243
+ const subtitle = (() => {
1244
+ if (remaining <= 0) return "Your bundle is complete";
1245
+ const pct = bundle.discountConfig.discountType === "percentage" && bundle.discountConfig.discountValue ? Math.round(bundle.discountConfig.discountValue) : 0;
1246
+ return pct > 0 ? `Choose ${remaining} more to unlock ${pct}% off` : `Choose ${remaining} more to complete your bundle`;
1247
+ })();
1248
+ const filtered = (0, import_react11.useMemo)(() => {
1249
+ const nq = normalizeText(query.trim());
1250
+ return eligible.filter((ep) => {
1251
+ const matchesQuery = !nq || normalizeText(ep.product.title).includes(nq);
1252
+ const matchesType = activeType === FILTERS_ALL || (ep.product.productType ?? "") === activeType;
1253
+ return matchesQuery && matchesType;
1254
+ });
1255
+ }, [eligible, query, activeType]);
1256
+ const showEmpty = filtered.length === 0 && query.trim().length > 0;
1257
+ function onOverlayMouseDown(e) {
1258
+ mouseDownTarget.current = e.target;
1259
+ }
1260
+ function onOverlayMouseUp(e) {
1261
+ if (e.target === overlayRef.current && mouseDownTarget.current === overlayRef.current) {
1262
+ onClose();
1263
+ }
1264
+ mouseDownTarget.current = null;
1265
+ }
1266
+ const titleId = `lb-modal-title-${sanitizeId(bundle.id)}`;
1267
+ const modal = (
1268
+ // The overlay is a backdrop: click-outside-to-close is a mouse convenience;
1269
+ // keyboard users close via Escape (handled by useFocusTrap), and the dialog
1270
+ // inside carries the interactive role + focus trap. So the static-element
1271
+ // interaction lint does not apply to this backdrop.
1272
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
1273
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1274
+ "div",
1275
+ {
1276
+ ref: (el) => {
1277
+ overlayRef.current = el;
1278
+ styleVarsRef(el);
1279
+ },
1280
+ className: `lb-mix-match__modal-overlay${open ? " lb-mix-match__modal-overlay--open" : ""}`,
1281
+ "data-modal-overlay": true,
1282
+ "data-bundle-gid": bundle.id,
1283
+ onMouseDown: onOverlayMouseDown,
1284
+ onMouseUp: onOverlayMouseUp,
1285
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1286
+ "div",
1287
+ {
1288
+ ref: dialogRef,
1289
+ className: `lb-mix-match__modal${showFilters ? "" : " lb-mix-match__modal--filters-hidden"}`,
1290
+ role: "dialog",
1291
+ "aria-modal": "true",
1292
+ "aria-labelledby": titleId,
1293
+ tabIndex: -1,
1294
+ children: [
1295
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-header", children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-header-top", children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-heading", children: [
1298
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h4", { className: "lb-mix-match__modal-title", id: titleId, children: "Add to your bundle" }),
1299
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-mix-match__modal-subtitle", "data-modal-subtitle": true, children: subtitle })
1300
+ ] }),
1301
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1302
+ "button",
1303
+ {
1304
+ type: "button",
1305
+ className: "lb-mix-match__modal-close",
1306
+ "data-modal-close": true,
1307
+ "aria-label": "Close",
1308
+ onClick: onClose,
1309
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {})
1310
+ }
1311
+ )
1312
+ ] }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1314
+ "div",
1315
+ {
1316
+ className: "lb-mix-match__progress lb-mix-match__modal-progress",
1317
+ "data-progress": true,
1318
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1319
+ "div",
1320
+ {
1321
+ className: "lb-mix-match__progress-segments",
1322
+ role: "progressbar",
1323
+ "aria-valuenow": shownUnits,
1324
+ "aria-valuemin": 0,
1325
+ "aria-valuemax": requiredQty,
1326
+ children: Array.from({ length: requiredQty }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1327
+ "span",
1328
+ {
1329
+ className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
1330
+ "data-progress-segment": true
1331
+ },
1332
+ i
1333
+ ))
1334
+ }
1335
+ )
1336
+ }
1337
+ )
1338
+ ] }),
1339
+ wc.showSearch && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-search", children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1341
+ "input",
1342
+ {
1343
+ type: "text",
1344
+ className: "lb-mix-match__modal-search-input",
1345
+ "data-modal-search": true,
1346
+ role: "searchbox",
1347
+ "aria-label": "Search products",
1348
+ placeholder: "Search products",
1349
+ autoComplete: "off",
1350
+ value: query,
1351
+ onChange: (e) => setQuery(e.target.value)
1352
+ }
1353
+ ),
1354
+ query.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1355
+ "button",
1356
+ {
1357
+ type: "button",
1358
+ className: "lb-mix-match__modal-search-clear",
1359
+ "data-modal-search-clear": true,
1360
+ "aria-label": "Clear search",
1361
+ onClick: () => setQuery(""),
1362
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchClearIcon, {})
1363
+ }
1364
+ )
1365
+ ] }),
1366
+ showFilters && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1367
+ "div",
1368
+ {
1369
+ className: "lb-mix-match__filters",
1370
+ "data-modal-filters": true,
1371
+ role: "group",
1372
+ "aria-label": "Filter by product type",
1373
+ children: [FILTERS_ALL, ...productTypes].map((value) => {
1374
+ const isActive = value === activeType;
1375
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1376
+ "button",
1377
+ {
1378
+ type: "button",
1379
+ className: `lb-mix-match__filter${isActive ? " lb-mix-match__filter--active" : ""}`,
1380
+ "data-filter": value,
1381
+ "aria-pressed": isActive,
1382
+ onClick: () => setActiveType(value),
1383
+ children: value === FILTERS_ALL ? "All" : value
1384
+ },
1385
+ value
1386
+ );
1387
+ })
1388
+ }
1389
+ ),
1390
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, children: filtered.map((ep) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1391
+ MixMatchPickerRow,
1392
+ {
1393
+ bundle,
1394
+ eligible: ep,
1395
+ currency,
1396
+ showStepper,
1397
+ selections,
1398
+ atCapacity,
1399
+ remainingUnits: remaining,
1400
+ onAdd,
1401
+ onUpdateQuantity,
1402
+ onRemoveVariant,
1403
+ canRemoveVariant,
1404
+ swapUnitsFor,
1405
+ onSwap
1406
+ },
1407
+ ep.product.id
1408
+ )) }),
1409
+ showEmpty && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-mix-match__modal-empty", "data-modal-empty": true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: "No products match your search" }) }),
1410
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "data-modal-live": true, "aria-live": "polite", className: "lb-visually-hidden", children: `${filtered.length} products shown` }),
1411
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-footer", children: [
1412
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1413
+ "span",
1414
+ {
1415
+ className: "lb-mix-match__modal-footer-count",
1416
+ "data-modal-footer-count": true,
1417
+ "aria-live": "polite",
1418
+ children: `${shownUnits} of ${requiredQty} added`
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1422
+ "button",
1423
+ {
1424
+ type: "button",
1425
+ className: "lb-mix-match__modal-done",
1426
+ "data-modal-done": true,
1427
+ onClick: onClose,
1428
+ children: "Done"
1429
+ }
1430
+ )
1431
+ ] })
1432
+ ]
1433
+ }
1434
+ )
1435
+ }
1436
+ )
1437
+ );
1438
+ if (typeof document === "undefined") return null;
1439
+ return (0, import_react_dom.createPortal)(modal, document.body);
1440
+ }
1441
+ function MixMatchPickerRow({
1442
+ bundle,
1443
+ eligible,
1444
+ currency,
1445
+ showStepper,
1446
+ selections,
1447
+ atCapacity,
1448
+ remainingUnits,
1449
+ onAdd,
1450
+ onUpdateQuantity,
1451
+ onRemoveVariant,
1452
+ canRemoveVariant,
1453
+ swapUnitsFor,
1454
+ onSwap
1455
+ }) {
1456
+ const { product, variants, isOos } = eligible;
1457
+ const rule = ruleFor(bundle, product.id);
1458
+ const optionNames = (0, import_react11.useMemo)(() => {
1459
+ const first = variants[0];
1460
+ return first ? first.selectedOptions.map((o) => o.name) : [];
1461
+ }, [variants]);
1462
+ const showPicker = variants.length > 1 && optionNames.length > 0;
1463
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1464
+ const currentVariant = selectedVariant ?? variants.find((v) => (0, import_core10.isVariantFulfillable)(v, rule.min)) ?? eligible.firstAvailableVariant ?? variants[0] ?? null;
1465
+ const alreadyInBundle = (0, import_react11.useMemo)(() => {
1466
+ if (!currentVariant) return 0;
1467
+ let sum = 0;
1468
+ for (const s of selections) {
1469
+ if (s.productId === product.id && s.variantId === currentVariant.id) {
1470
+ sum += s.quantity;
1471
+ }
1472
+ }
1473
+ return sum;
1474
+ }, [selections, product.id, currentVariant]);
1475
+ const committedSelection = (0, import_react11.useMemo)(
1476
+ () => currentVariant ? selections.find((s) => s.variantId === currentVariant.id) ?? null : null,
1477
+ [selections, currentVariant]
1478
+ );
1479
+ const variantInBundle = committedSelection !== null;
1480
+ const committedQty = committedSelection?.quantity ?? 0;
1481
+ const productOtherUnits = (0, import_react11.useMemo)(() => {
1482
+ if (!currentVariant) return 0;
1483
+ let sum = 0;
1484
+ for (const s of selections) {
1485
+ if (s.productId === product.id && s.variantId !== currentVariant.id) {
1486
+ sum += s.quantity;
1487
+ }
1488
+ }
1489
+ return sum;
1490
+ }, [selections, product.id, currentVariant]);
1491
+ const swapUnits = currentVariant ? swapUnitsFor?.(product.id, currentVariant.id) ?? 0 : 0;
1492
+ const inSwapState = swapUnits >= rule.min;
1493
+ const spotCeiling = inSwapState ? swapUnits : Math.min(rule.max - productOtherUnits, committedQty + remainingUnits);
1494
+ const cap = currentVariant ? (0, import_core10.maxAddableQuantity)(
1495
+ currentVariant,
1496
+ spotCeiling,
1497
+ Math.max(0, alreadyInBundle - committedQty)
1498
+ ) : 0;
1499
+ const stepperMax = Math.max(rule.min, cap);
1500
+ const [qty, setQty] = (0, import_react11.useState)(rule.min);
1501
+ (0, import_react11.useEffect)(() => {
1502
+ setQty((prev) => Math.max(rule.min, Math.min(prev, stepperMax)));
1503
+ }, [stepperMax, rule.min]);
1504
+ const currentVariantId = currentVariant?.id ?? null;
1505
+ (0, import_react11.useEffect)(() => {
1506
+ if (!variantInBundle) setQty(rule.min);
1507
+ }, [variantInBundle, currentVariantId, rule.min]);
1508
+ if (!currentVariant) return null;
1509
+ const thumbImage = currentVariant.image ?? product.featuredImage ?? null;
1510
+ const unitPriceText = (0, import_core10.formatUnitPrice)(
1511
+ currentVariant.unitPrice,
1512
+ currentVariant.unitPriceMeasurement,
1513
+ currency
1514
+ );
1515
+ const noStock = cap < rule.min;
1516
+ const needsMoreSpots = !variantInBundle && !inSwapState && rule.min > remainingUnits;
1517
+ const addDisabled = !variantInBundle && (inSwapState ? noStock : atCapacity || noStock || needsMoreSpots);
1518
+ const lockedRequired = variantInBundle && currentVariant != null && !(canRemoveVariant?.(currentVariant.id) ?? true);
1519
+ const stepperDisabled = isOos || !variantInBundle && addDisabled;
1520
+ const shownQty = variantInBundle ? committedQty : qty;
1521
+ function handleAddOrRemove() {
1522
+ if (variantInBundle && currentVariant) {
1523
+ onRemoveVariant(currentVariant.id);
1524
+ return;
1525
+ }
1526
+ if (!currentVariant || noStock) return;
1527
+ const pickedQty = showStepper ? qty : rule.min;
1528
+ const item = {
1529
+ productId: product.id,
1530
+ variantId: currentVariant.id,
1531
+ title: product.title,
1532
+ url: `/products/${product.handle}`,
1533
+ variantTitle: currentVariant.title,
1534
+ featuredImage: thumbImage?.url ?? null,
1535
+ price: parseFloat(currentVariant.price.amount),
1536
+ compareAtPrice: currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null,
1537
+ unitPrice: unitPriceText,
1538
+ quantity: pickedQty
1539
+ };
1540
+ if (inSwapState && onSwap) {
1541
+ onSwap({ ...item, quantity: Math.min(pickedQty, swapUnits) });
1542
+ return;
1543
+ }
1544
+ if (atCapacity || needsMoreSpots) return;
1545
+ onAdd(item);
1546
+ }
1547
+ const priceCents = parseFloat(currentVariant.price.amount);
1548
+ const compareCents = currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null;
1549
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1550
+ "div",
1551
+ {
1552
+ className: `lb-mix-match__modal-product${isOos ? " lb-mix-match__modal-product--sold-out" : ""}${variantInBundle ? " lb-mix-match__modal-product--in-bundle" : ""}`,
1553
+ "data-product-item": true,
1554
+ "data-title": normalizeText(product.title),
1555
+ "data-type": product.productType ?? "",
1556
+ "aria-disabled": isOos || void 0,
1557
+ children: [
1558
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-product-thumb", children: [
1559
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1560
+ "img",
1561
+ {
1562
+ src: thumbImage.url,
1563
+ alt: thumbImage.altText ?? product.title,
1564
+ loading: "lazy"
1565
+ }
1566
+ ),
1567
+ variantInBundle && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-mix-match__modal-added-badge", children: "Added" })
1568
+ ] }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-product-info", children: [
1570
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-mix-match__modal-product-title", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1571
+ "a",
1572
+ {
1573
+ href: `/products/${product.handle}`,
1574
+ target: "_blank",
1575
+ rel: "noopener noreferrer",
1576
+ children: product.title
1577
+ }
1578
+ ) }),
1579
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "lb-mix-match__modal-product-price", children: [
1580
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "data-row-price-sale": true, children: (0, import_core10.formatMoney)(priceCents, currency) }),
1581
+ compareCents !== null && compareCents > priceCents && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("s", { className: "lb-mix-match__modal-product-compare", "data-row-compare": true, children: (0, import_core10.formatMoney)(compareCents, currency) })
1582
+ ] }),
1583
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price", children: unitPriceText }),
1584
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle-variant-option-groups", children: optionNames.map((optionName, optionIndex) => {
1585
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1586
+ value: o.value,
1587
+ label: o.value,
1588
+ disabled: o.disabled
1589
+ }));
1590
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1591
+ "div",
1592
+ {
1593
+ className: "lb-bundle-variant-option-group",
1594
+ children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle-variant-option-label", children: optionName }),
1596
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1597
+ VariantDropdown,
1598
+ {
1599
+ className: "lb-mix-match__variant-select-dropdown",
1600
+ options: dropdownOptions,
1601
+ value: selectedValues[optionIndex] ?? null,
1602
+ onChange: (v) => setOptionValue(optionIndex, v),
1603
+ ariaLabel: optionName
1604
+ }
1605
+ )
1606
+ ]
1607
+ },
1608
+ optionName
1609
+ );
1610
+ }) }),
1611
+ !showPicker && variants.length === 1 && currentVariant.title !== "Default Title" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-mix-match__filled-variant", children: currentVariant.title }),
1612
+ !isOos && needsMoreSpots && remainingUnits > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-mix-match__modal-needs-spots", children: rule.min === 1 ? "Needs 1 spot" : `Needs ${rule.min} spots` }),
1613
+ isOos ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-mix-match__modal-sold-out-label", children: "Sold out" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-mix-match__modal-product-actions", children: [
1614
+ showStepper && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle-variant-option-group lb-mix-match__qty-stepper-group", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1615
+ "div",
1616
+ {
1617
+ className: "lb-mix-match__qty-stepper",
1618
+ role: "group",
1619
+ "aria-label": "Quantity",
1620
+ children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1622
+ "button",
1623
+ {
1624
+ type: "button",
1625
+ className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus",
1626
+ "aria-label": "Decrease quantity",
1627
+ disabled: stepperDisabled || shownQty <= rule.min,
1628
+ onClick: () => {
1629
+ if (variantInBundle && currentVariant) {
1630
+ onUpdateQuantity(
1631
+ product.id,
1632
+ currentVariant.id,
1633
+ Math.max(rule.min, committedQty - 1)
1634
+ );
1635
+ } else {
1636
+ setQty((q) => Math.max(rule.min, q - 1));
1637
+ }
1638
+ },
1639
+ children: "\u2212"
1640
+ }
1641
+ ),
1642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1643
+ "span",
1644
+ {
1645
+ className: "lb-mix-match__qty-stepper-value",
1646
+ "data-row-qty": true,
1647
+ "aria-live": "polite",
1648
+ children: shownQty
1649
+ }
1650
+ ),
1651
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1652
+ "button",
1653
+ {
1654
+ type: "button",
1655
+ className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus",
1656
+ "aria-label": "Increase quantity",
1657
+ disabled: stepperDisabled || shownQty >= stepperMax,
1658
+ onClick: () => {
1659
+ if (variantInBundle && currentVariant) {
1660
+ onUpdateQuantity(
1661
+ product.id,
1662
+ currentVariant.id,
1663
+ Math.min(stepperMax, committedQty + 1)
1664
+ );
1665
+ } else {
1666
+ setQty((q) => Math.min(stepperMax, q + 1));
1667
+ }
1668
+ },
1669
+ children: "+"
1670
+ }
1671
+ )
1672
+ ]
1673
+ }
1674
+ ) }),
1675
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1676
+ "button",
1677
+ {
1678
+ type: "button",
1679
+ className: `lb-mix-match__modal-add${variantInBundle ? " lb-mix-match__modal-add--added" : ""}${lockedRequired ? " lb-mix-match__modal-add--required" : ""}`,
1680
+ "data-add-product": true,
1681
+ "aria-label": variantInBundle ? lockedRequired ? `${product.title} is required and cannot be removed` : `Remove ${product.title}` : `${inSwapState ? "Swap" : "Add"} ${product.title}`,
1682
+ disabled: addDisabled || lockedRequired,
1683
+ onClick: handleAddOrRemove,
1684
+ children: variantInBundle ? lockedRequired ? "Required" : "Remove" : inSwapState ? "Swap" : "Add"
1685
+ }
1686
+ )
1687
+ ] })
1688
+ ] })
1689
+ ]
1690
+ }
1691
+ );
1692
+ }
1693
+ function CloseIcon() {
1694
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1695
+ "svg",
1696
+ {
1697
+ width: "20",
1698
+ height: "20",
1699
+ viewBox: "0 0 20 20",
1700
+ fill: "none",
1701
+ xmlns: "http://www.w3.org/2000/svg",
1702
+ "aria-hidden": "true",
1703
+ children: [
1704
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
1705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
1706
+ ]
1707
+ }
1708
+ );
1709
+ }
1710
+ function SearchClearIcon() {
1711
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1712
+ "svg",
1713
+ {
1714
+ width: "16",
1715
+ height: "16",
1716
+ viewBox: "0 0 20 20",
1717
+ fill: "currentColor",
1718
+ xmlns: "http://www.w3.org/2000/svg",
1719
+ "aria-hidden": "true",
1720
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z" })
1721
+ }
1722
+ );
1723
+ }
1724
+
1725
+ // src/components/MixMatchBundle.tsx
1726
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1727
+ function ruleFor2(bundle, productId) {
1728
+ return bundle.productRules[productId] ?? import_core11.DEFAULT_PRODUCT_RULE;
1729
+ }
1730
+ function canRemoveItem(bundle, selections, item) {
1731
+ const rule = ruleFor2(bundle, item.productId);
1732
+ if (!rule.required) return true;
1733
+ const unitsElsewhere = selections.filter((s) => s.productId === item.productId && s !== item).reduce((sum, s) => sum + s.quantity, 0);
1734
+ return unitsElsewhere >= rule.min;
1735
+ }
1736
+ function swapUnitsForItem(bundle, selections, productId, variantId) {
1737
+ const rule = ruleFor2(bundle, productId);
1738
+ if (!rule.required || rule.max !== rule.min) return 0;
1739
+ if (selections.some((s) => s.variantId === variantId)) return 0;
1740
+ return selections.filter((s) => s.productId === productId && s.variantId !== variantId).reduce((sum, s) => sum + s.quantity, 0);
1741
+ }
1742
+ function MixMatchBundle(props) {
1743
+ const {
1744
+ shopDomain,
1745
+ storefrontAccessToken,
1746
+ bundleGid,
1747
+ appUrl,
1748
+ analyticsEnabled,
1749
+ onAddToCart,
1750
+ onError,
1751
+ className
1752
+ } = props;
1753
+ const result = useBundleData({
1754
+ shopDomain,
1755
+ storefrontAccessToken,
1756
+ bundleGid
1757
+ });
1758
+ const { outOfStockBehavior } = useShopSettings({
1759
+ shopDomain,
1760
+ storefrontAccessToken
1761
+ });
1762
+ const { elementRef, trackAddToCart } = useAnalytics({
1763
+ shopDomain,
1764
+ appUrl: appUrl ?? `https://${shopDomain}`,
1765
+ bundleGid,
1766
+ bundleType: "mix_match",
1767
+ enabled: analyticsEnabled !== false
1768
+ });
1769
+ const [selections, setSelections] = (0, import_react12.useState)([]);
1770
+ const [seededFor, setSeededFor] = (0, import_react12.useState)(null);
1771
+ const [pickerOpen, setPickerOpen] = (0, import_react12.useState)(false);
1772
+ const [addingToCart, setAddingToCart] = (0, import_react12.useState)(false);
1773
+ const [cartError, setCartError] = (0, import_react12.useState)(null);
1774
+ const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
1775
+ const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1776
+ const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1777
+ const slotsEdgeFadeRef = useEdgeFade();
1778
+ (0, import_react12.useEffect)(() => {
1779
+ if (result.status === "error") {
1780
+ onError?.(result.error);
1781
+ return;
1782
+ }
1783
+ if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
1784
+ onError?.(
1785
+ new Error(
1786
+ `MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
1787
+ )
1788
+ );
1789
+ }
1790
+ }, [result, onError]);
1791
+ (0, import_react12.useEffect)(() => {
1792
+ if (!bundle || seededFor === bundle.id) return;
1793
+ setSeededFor(bundle.id);
1794
+ const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
1795
+ const seeds = [];
1796
+ for (const [pid, rule] of Object.entries(bundle.productRules)) {
1797
+ if (!rule.required) continue;
1798
+ const product = bundle.products.find((p) => p.id === pid);
1799
+ const variant = product?.variants.nodes.find(
1800
+ (v) => (0, import_core11.isVariantFulfillable)(v, rule.min)
1801
+ );
1802
+ if (!product || !variant) continue;
1803
+ seeds.push({
1804
+ productId: product.id,
1805
+ variantId: variant.id,
1806
+ title: product.title,
1807
+ url: `/products/${product.handle}`,
1808
+ variantTitle: variant.title,
1809
+ featuredImage: variant.image?.url ?? product.featuredImage?.url ?? null,
1810
+ price: parseFloat(variant.price.amount),
1811
+ compareAtPrice: variant.compareAtPrice ? parseFloat(variant.compareAtPrice.amount) : null,
1812
+ unitPrice: (0, import_core11.formatUnitPrice)(
1813
+ variant.unitPrice,
1814
+ variant.unitPriceMeasurement,
1815
+ seedCurrency
1816
+ ),
1817
+ quantity: rule.min
1818
+ });
1819
+ }
1820
+ if (seeds.length > 0) setSelections(seeds);
1821
+ }, [bundle, seededFor]);
1822
+ const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
1823
+ const requiredUnits = bundle?.minQuantity ?? 0;
1824
+ const totalQuantity = (0, import_react12.useMemo)(
1825
+ () => selections.reduce((sum, s) => sum + s.quantity, 0),
1826
+ [selections]
1827
+ );
1828
+ const meetsMinUnits = totalQuantity >= requiredUnits && requiredUnits > 0;
1829
+ const valid = !!bundle && meetsMinUnits;
1830
+ const shownUnits = Math.min(totalQuantity, requiredUnits);
1831
+ const remaining = Math.max(0, requiredUnits - shownUnits);
1832
+ const summary = (0, import_react12.useMemo)(() => {
1833
+ let comparePrice = 0;
1834
+ let salePrice = 0;
1835
+ for (const sel of selections) {
1836
+ if (sel.quantity <= 0) continue;
1837
+ comparePrice += sel.price * sel.quantity;
1838
+ if (bundle) {
1839
+ salePrice += (0, import_core11.calculateDiscount)(
1840
+ sel.price,
1841
+ bundle.discountConfig.discountType,
1842
+ bundle.discountConfig.discountValue
1843
+ ) * sel.quantity;
1844
+ }
1845
+ }
1846
+ const savings = Math.max(0, comparePrice - salePrice);
1847
+ const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
1848
+ return { comparePrice, salePrice, savings, savingsPercent };
1849
+ }, [selections, bundle]);
1850
+ const addSelection = (0, import_react12.useCallback)(
1851
+ (item) => {
1852
+ setSelections((prev) => {
1853
+ const units = prev.reduce((sum, s) => sum + s.quantity, 0);
1854
+ if (units + item.quantity > requiredUnits) return prev;
1855
+ if (prev.some((s) => s.variantId === item.variantId)) return prev;
1856
+ return [...prev, item];
1857
+ });
1858
+ },
1859
+ [requiredUnits]
1860
+ );
1861
+ const updateQuantity = (0, import_react12.useCallback)(
1862
+ (productId, variantId, quantity) => {
1863
+ setSelections((prev) => {
1864
+ const idx = prev.findIndex(
1865
+ (s) => s.productId === productId && s.variantId === variantId
1866
+ );
1867
+ if (idx === -1) return prev;
1868
+ const otherUnits = prev.reduce(
1869
+ (sum, s, i) => i === idx ? sum : sum + s.quantity,
1870
+ 0
1871
+ );
1872
+ const clamped = Math.max(
1873
+ 1,
1874
+ Math.min(quantity, requiredUnits - otherUnits)
1875
+ );
1876
+ if (clamped === prev[idx].quantity) return prev;
1877
+ const next = prev.slice();
1878
+ next[idx] = { ...next[idx], quantity: clamped };
1879
+ return next;
1880
+ });
1881
+ },
1882
+ [requiredUnits]
1883
+ );
1884
+ const swapSelection = (0, import_react12.useCallback)(
1885
+ (item) => {
1886
+ setSelections((prev) => {
1887
+ if (!bundle) return prev;
1888
+ const swapUnits = swapUnitsForItem(
1889
+ bundle,
1890
+ prev,
1891
+ item.productId,
1892
+ item.variantId
1893
+ );
1894
+ if (swapUnits <= 0) return prev;
1895
+ const qty = Math.min(Math.max(1, item.quantity), swapUnits);
1896
+ let freed = 0;
1897
+ const next = [];
1898
+ for (const s of prev) {
1899
+ if (freed < qty && s.productId === item.productId && s.variantId !== item.variantId) {
1900
+ const take = Math.min(s.quantity, qty - freed);
1901
+ freed += take;
1902
+ if (s.quantity > take) {
1903
+ next.push({ ...s, quantity: s.quantity - take });
1904
+ }
1905
+ } else {
1906
+ next.push(s);
1907
+ }
1908
+ }
1909
+ if (freed === 0) return prev;
1910
+ next.push({ ...item, quantity: freed });
1911
+ return next;
1912
+ });
1913
+ },
1914
+ [bundle]
1915
+ );
1916
+ const removeVariant = (0, import_react12.useCallback)(
1917
+ (variantId) => {
1918
+ setSelections((prev) => {
1919
+ const idx = prev.findIndex((s) => s.variantId === variantId);
1920
+ if (idx === -1) return prev;
1921
+ if (bundle && !canRemoveItem(bundle, prev, prev[idx])) return prev;
1922
+ const next = prev.slice();
1923
+ next.splice(idx, 1);
1924
+ return next;
1925
+ });
1926
+ },
1927
+ [bundle]
1928
+ );
1929
+ const removeSlot = (0, import_react12.useCallback)(
1930
+ (index) => {
1931
+ setSelections((prev) => {
1932
+ if (index < 0 || index >= prev.length) return prev;
1933
+ if (bundle && !canRemoveItem(bundle, prev, prev[index])) return prev;
1934
+ const next = prev.slice();
1935
+ next.splice(index, 1);
1936
+ return next;
1937
+ });
1938
+ },
1939
+ [bundle]
1940
+ );
1941
+ const handleAddToCart = (0, import_react12.useCallback)(async () => {
1942
+ if (!bundle || !valid) return;
1943
+ const grouped = /* @__PURE__ */ new Map();
1944
+ for (const sel of selections) {
1945
+ if (sel.quantity <= 0) continue;
1946
+ const key = `${sel.productId}::${sel.variantId}`;
1947
+ const existing = grouped.get(key);
1948
+ if (existing) existing.quantity += sel.quantity;
1949
+ else grouped.set(key, { variantId: sel.variantId, quantity: sel.quantity });
1950
+ }
1951
+ const lines = Array.from(grouped.values()).map((line) => ({
1952
+ merchandiseId: line.variantId,
1953
+ quantity: line.quantity,
1954
+ attributes: [
1955
+ { key: "_lime_bundle_gid", value: bundle.id },
1956
+ { key: "_lime_bundle_type", value: bundle.bundleType }
1957
+ ]
1958
+ }));
1959
+ setAddingToCart(true);
1960
+ setCartError(null);
1961
+ try {
1962
+ await onAddToCart(lines);
1963
+ const totalPrice = selections.reduce(
1964
+ (sum, sel) => sum + sel.price * sel.quantity,
1965
+ 0
1966
+ );
1967
+ trackAddToCart({
1968
+ quantity: totalQuantity,
1969
+ totalPrice: Math.round(totalPrice * 100) / 100
1970
+ });
1971
+ setSelections([]);
1972
+ } catch (err) {
1973
+ const error = err instanceof Error ? err : new Error(String(err));
1974
+ setCartError(error.message);
1975
+ onError?.(error);
1976
+ } finally {
1977
+ setAddingToCart(false);
1978
+ }
1979
+ }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
1980
+ if (result.status === "loading") {
1981
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1982
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
1983
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
1984
+ ] });
1985
+ }
1986
+ if (result.status === "error") return null;
1987
+ if (!bundle) return null;
1988
+ const inStockCount = countInStockProducts(bundle);
1989
+ if (requiredUnits > 0 && inStockCount < requiredUnits) return null;
1990
+ const requiredBlocked = Object.entries(bundle.productRules).some(
1991
+ ([pid, rule]) => {
1992
+ if (!rule.required) return false;
1993
+ const product = bundle.products.find((p) => p.id === pid);
1994
+ return !product?.variants.nodes.some(
1995
+ (v) => (0, import_core11.isVariantFulfillable)(v, rule.min)
1996
+ );
1997
+ }
1998
+ );
1999
+ if (requiredBlocked) return null;
2000
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
2001
+ const ctaLabel = addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText || "Add to cart";
2002
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2003
+ "div",
2004
+ {
2005
+ ref: (el) => {
2006
+ elementRef(el);
2007
+ setConfigVarsRef(el);
2008
+ },
2009
+ className: `lb-bundle lb-bundle--mix-match lb-mix-match ${className ?? ""}`,
2010
+ role: "region",
2011
+ "aria-label": bundle.title,
2012
+ "data-required-quantity": requiredUnits,
2013
+ children: [
2014
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle-header", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle-header__content", children: [
2015
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "lb-bundle-title", children: bundle.title }),
2016
+ bundle.description && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle-subtitle", children: bundle.description })
2017
+ ] }) }),
2018
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-mix-match__progress", "data-progress": true, children: [
2019
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2020
+ "div",
2021
+ {
2022
+ className: "lb-mix-match__progress-segments",
2023
+ role: "progressbar",
2024
+ "aria-valuenow": shownUnits,
2025
+ "aria-valuemin": 0,
2026
+ "aria-valuemax": requiredUnits,
2027
+ children: Array.from({ length: requiredUnits }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2028
+ "span",
2029
+ {
2030
+ className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
2031
+ "data-progress-segment": true
2032
+ },
2033
+ i
2034
+ ))
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-mix-match__progress-labels", children: [
2038
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2039
+ "span",
2040
+ {
2041
+ className: "lb-mix-match__progress-count",
2042
+ "data-progress-count": true,
2043
+ "aria-live": "polite",
2044
+ children: `${shownUnits} of ${requiredUnits} added`
2045
+ }
2046
+ ),
2047
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2048
+ "span",
2049
+ {
2050
+ className: "lb-mix-match__progress-remaining",
2051
+ "data-progress-remaining": true,
2052
+ "aria-live": "polite",
2053
+ children: remaining > 0 ? `${remaining} more to go` : "Complete"
2054
+ }
2055
+ )
2056
+ ] })
2057
+ ] }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2059
+ "div",
2060
+ {
2061
+ className: "lb-mix-match__slots lb-edge-fade",
2062
+ "data-selection-slots": true,
2063
+ ref: slotsEdgeFadeRef,
2064
+ children: selections.map((item, index) => (
2065
+ /* Whole card reopens the picker to edit this pick. The inner title
2066
+ button provides the keyboard/AT path (its activation bubbles
2067
+ here); × stops propagation. */
2068
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- keyboard path is the inner "Edit selection" title button whose activation bubbles to this handler
2069
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2070
+ "div",
2071
+ {
2072
+ className: "lb-mix-match__slot lb-mix-match__slot--filled",
2073
+ onClick: () => setPickerOpen(true),
2074
+ children: [
2075
+ item.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle-thumbnail", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: item.featuredImage, alt: item.title, loading: "lazy" }) }),
2076
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-mix-match__filled-info", children: [
2077
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2078
+ "button",
2079
+ {
2080
+ type: "button",
2081
+ className: "lb-mix-match__filled-title",
2082
+ "aria-label": `Edit selection: ${item.title}`,
2083
+ children: item.title
2084
+ }
2085
+ ),
2086
+ item.variantTitle && item.variantTitle !== "Default Title" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-mix-match__filled-variant", children: item.variantTitle }),
2087
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-mix-match__filled-price", children: [
2088
+ item.compareAtPrice !== null && item.compareAtPrice > item.price && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("s", { className: "lb-mix-match__filled-compare", children: (0, import_core11.formatMoney)(item.compareAtPrice, currency) }),
2089
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-product-price", children: (0, import_core11.formatMoney)(item.price, currency) }),
2090
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-qty-inline", children: `\xD7${item.quantity}` })
2091
+ ] }),
2092
+ item.unitPrice && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-product-unit-price", children: item.unitPrice })
2093
+ ] }),
2094
+ canRemoveItem(bundle, selections, item) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2095
+ "button",
2096
+ {
2097
+ type: "button",
2098
+ className: "lb-mix-match__slot-remove",
2099
+ "aria-label": `Remove ${item.title}`,
2100
+ onClick: (e) => {
2101
+ e.stopPropagation();
2102
+ removeSlot(index);
2103
+ },
2104
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CloseIcon2, {})
2105
+ }
2106
+ ) : (
2107
+ /* Required pick at its floor — quiet state chip instead of
2108
+ the ×; removal returns once another slot covers the
2109
+ product's minimum (variant swap flow). */
2110
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-mix-match__slot-required", children: "Required" })
2111
+ )
2112
+ ]
2113
+ },
2114
+ `${item.productId}:${item.variantId}:${index}`
2115
+ )
2116
+ ))
2117
+ }
2118
+ ),
2119
+ totalQuantity < requiredUnits && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2120
+ "button",
2121
+ {
2122
+ type: "button",
2123
+ className: "lb-mix-match__add-product",
2124
+ "data-add-product-trigger": true,
2125
+ onClick: () => setPickerOpen(true),
2126
+ children: [
2127
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-mix-match__add-product-icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlusIcon, {}) }),
2128
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-mix-match__add-product-label", children: "Add a product" })
2129
+ ]
2130
+ }
2131
+ ),
2132
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle-divider" }),
2133
+ selections.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle-summary", "data-pricing-section": true, children: [
2134
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle-summary__text", children: [
2135
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-summary__label", children: "Bundle total" }),
2136
+ bundle.widgetConfig.savingsBar.visible && summary.savings > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
2137
+ "You save",
2138
+ " ",
2139
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "data-savings-amount": true, children: (0, import_core11.formatMoney)(summary.savings, currency) }),
2140
+ " ",
2141
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { "data-savings-percent": true, children: [
2142
+ "(",
2143
+ summary.savingsPercent,
2144
+ "%)"
2145
+ ] })
2146
+ ] })
2147
+ ] }),
2148
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle-summary__prices", children: [
2149
+ bundle.widgetConfig.pricing.showComparePrice && summary.savings > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: (0, import_core11.formatMoney)(summary.comparePrice, currency) }),
2150
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: (0, import_core11.formatMoney)(summary.salePrice, currency) })
2151
+ ] })
2152
+ ] }),
2153
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
2154
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2155
+ "button",
2156
+ {
2157
+ className: "lb-bundle__cta",
2158
+ onClick: handleAddToCart,
2159
+ disabled: addingToCart || !valid,
2160
+ "aria-busy": addingToCart,
2161
+ children: ctaLabel
2162
+ }
2163
+ ),
2164
+ pickerOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2165
+ MixMatchPicker,
2166
+ {
2167
+ outOfStockBehavior,
2168
+ bundle,
2169
+ currency,
2170
+ requiredQty: requiredUnits,
2171
+ showStepper,
2172
+ selections,
2173
+ onAdd: addSelection,
2174
+ onUpdateQuantity: updateQuantity,
2175
+ onRemoveVariant: removeVariant,
2176
+ canRemoveVariant: (variantId) => {
2177
+ const item = selections.find((s) => s.variantId === variantId);
2178
+ return item ? canRemoveItem(bundle, selections, item) : true;
2179
+ },
2180
+ swapUnitsFor: (productId, variantId) => swapUnitsForItem(bundle, selections, productId, variantId),
2181
+ onSwap: swapSelection,
2182
+ onClose: () => setPickerOpen(false),
2183
+ styleVarsRef: setModalConfigVarsRef
2184
+ }
2185
+ )
2186
+ ]
2187
+ }
2188
+ );
2189
+ }
2190
+ function countInStockProducts(bundle) {
2191
+ const seen = /* @__PURE__ */ new Set();
2192
+ let count = 0;
2193
+ for (const product of bundle.products) {
2194
+ if (seen.has(product.id)) continue;
2195
+ seen.add(product.id);
2196
+ const rule = ruleFor2(bundle, product.id);
2197
+ const fulfillable = product.variants.nodes.some(
2198
+ (v) => (0, import_core11.isVariantFulfillable)(v, rule.min)
2199
+ );
2200
+ if (fulfillable) count += 1;
2201
+ }
2202
+ return count;
2203
+ }
2204
+ function PlusIcon() {
2205
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2206
+ "svg",
2207
+ {
2208
+ width: "18",
2209
+ height: "18",
2210
+ viewBox: "0 0 18 18",
2211
+ fill: "none",
2212
+ xmlns: "http://www.w3.org/2000/svg",
2213
+ "aria-hidden": "true",
2214
+ children: [
2215
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
2216
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
2217
+ ]
2218
+ }
2219
+ );
2220
+ }
2221
+ function CloseIcon2() {
2222
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2223
+ "svg",
2224
+ {
2225
+ width: "16",
2226
+ height: "16",
2227
+ viewBox: "0 0 20 20",
2228
+ fill: "none",
2229
+ xmlns: "http://www.w3.org/2000/svg",
2230
+ "aria-hidden": "true",
2231
+ children: [
2232
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
2233
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
2234
+ ]
2235
+ }
2236
+ );
2237
+ }
2238
+
2239
+ // src/components/VolumeBundle.tsx
2240
+ var import_react13 = require("react");
2241
+ var import_core12 = require("@lime-bundles/core");
2242
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2243
+ function VolumeBundle(props) {
2244
+ const {
2245
+ shopDomain,
2246
+ storefrontAccessToken,
2247
+ bundleGid,
2248
+ appUrl,
2249
+ analyticsEnabled,
2250
+ onAddToCart,
2251
+ onError,
2252
+ className
2253
+ } = props;
2254
+ const result = useBundleData({
2255
+ shopDomain,
2256
+ storefrontAccessToken,
2257
+ bundleGid
2258
+ });
2259
+ const { elementRef, trackAddToCart } = useAnalytics({
2260
+ shopDomain,
2261
+ appUrl: appUrl ?? `https://${shopDomain}`,
2262
+ bundleGid,
2263
+ bundleType: "volume",
2264
+ enabled: analyticsEnabled !== false
2265
+ });
2266
+ const [quantity, setQuantity] = (0, import_react13.useState)(1);
2267
+ const [addingToCart, setAddingToCart] = (0, import_react13.useState)(false);
2268
+ const [cartError, setCartError] = (0, import_react13.useState)(null);
2269
+ const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
2270
+ const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
2271
+ const tiersEdgeFadeRef = useEdgeFade();
2272
+ (0, import_react13.useEffect)(() => {
2273
+ if (result.status === "error") {
2274
+ onError?.(result.error);
2275
+ return;
2276
+ }
2277
+ if (result.status === "success" && result.bundle.bundleType !== "volume") {
2278
+ onError?.(
2279
+ new Error(
2280
+ `VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
2281
+ )
2282
+ );
2283
+ }
2284
+ }, [result, onError]);
2285
+ const product = bundle?.products[0];
2286
+ const variants = (0, import_react13.useMemo)(() => product?.variants.nodes ?? [], [product]);
2287
+ const optionNames = (0, import_react13.useMemo)(() => {
2288
+ const first = variants[0];
2289
+ return first ? first.selectedOptions.map((o) => o.name) : [];
2290
+ }, [variants]);
2291
+ const showPicker = variants.length > 1 && optionNames.length > 0;
2292
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
2293
+ const minTierQty = bundle ? (0, import_core12.getMinTierQuantity)(bundle.volumeTiers) : 1;
2294
+ const isSoldOut = variants.length > 0 && !variants.some((v) => (0, import_core12.isVariantFulfillable)(v, minTierQty));
2295
+ const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core12.isVariantFulfillable)(v, minTierQty)) ?? variants[0];
2296
+ const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
2297
+ const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
2298
+ const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
2299
+ const unitPriceText = displayVariant ? (0, import_core12.formatUnitPrice)(
2300
+ displayVariant.unitPrice,
2301
+ displayVariant.unitPriceMeasurement,
2302
+ currency
2303
+ ) : null;
2304
+ const tierSavings = (0, import_react13.useMemo)(
2305
+ () => bundle ? (0, import_core12.calculateTierSavings)(
2306
+ bundle.volumeTiers,
2307
+ basePrice,
2308
+ quantity,
2309
+ bundle.discountConfig.discountType
2310
+ ) : [],
2311
+ [bundle, basePrice, quantity]
2312
+ );
2313
+ const activeTier = bundle ? (0, import_core12.getActiveTier)(bundle.volumeTiers, quantity) : null;
2314
+ const popularTierIndex = (0, import_react13.useMemo)(() => {
2315
+ if (!bundle?.widgetConfig.popularBadge.visible || tierSavings.length === 0) {
2316
+ return -1;
2317
+ }
2318
+ const pinned = bundle.widgetConfig.popularBadge.tierIndex;
2319
+ if (pinned !== void 0) {
2320
+ return Math.min(Math.max(pinned, 0), tierSavings.length - 1);
2321
+ }
2322
+ let best = 0;
2323
+ for (let i = 1; i < tierSavings.length; i++) {
2324
+ if (tierSavings[i].savings > tierSavings[best].savings) best = i;
2325
+ }
2326
+ return best;
2327
+ }, [bundle, tierSavings]);
2328
+ const activeUnitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
2329
+ const undiscountedTotal = basePrice * quantity;
2330
+ const volumeTotal = activeUnitPrice * quantity;
2331
+ const volumeSavings = Math.max(0, undiscountedTotal - volumeTotal);
2332
+ const volumeSavingsPercent = undiscountedTotal > 0 && volumeSavings > 0 ? Math.round(volumeSavings / undiscountedTotal * 100) : 0;
2333
+ const handleAddToCart = (0, import_react13.useCallback)(async () => {
2334
+ if (!bundle || !product) return;
2335
+ const variant = selectedVariant ?? product.variants.nodes.find(
2336
+ (v) => (0, import_core12.isVariantFulfillable)(v, quantity)
2337
+ );
2338
+ if (!variant) return;
2339
+ const lines = [
2340
+ {
2341
+ merchandiseId: variant.id,
2342
+ quantity,
2343
+ attributes: [
2344
+ { key: "_lime_bundle_gid", value: bundle.id },
2345
+ { key: "_lime_bundle_type", value: bundle.bundleType }
2346
+ ]
2347
+ }
2348
+ ];
2349
+ setAddingToCart(true);
2350
+ setCartError(null);
2351
+ try {
2352
+ await onAddToCart(lines);
2353
+ const unitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
2354
+ trackAddToCart({
2355
+ productId: product.id,
2356
+ quantity,
2357
+ totalPrice: Math.round(unitPrice * quantity * 100) / 100
2358
+ });
2359
+ } catch (err) {
2360
+ const error = err instanceof Error ? err : new Error(String(err));
2361
+ setCartError(error.message);
2362
+ onError?.(error);
2363
+ } finally {
2364
+ setAddingToCart(false);
2365
+ }
2366
+ }, [
2367
+ bundle,
2368
+ product,
2369
+ quantity,
2370
+ activeTier,
2371
+ basePrice,
2372
+ onAddToCart,
2373
+ onError,
2374
+ trackAddToCart,
2375
+ selectedVariant,
2376
+ tierSavings
2377
+ ]);
2378
+ if (result.status === "loading") {
2379
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
2380
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
2381
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
2382
+ ] });
2383
+ }
2384
+ if (result.status === "error") return null;
2385
+ if (!bundle) return null;
2386
+ if (!product) return null;
2387
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2388
+ "div",
2389
+ {
2390
+ ref: (el) => {
2391
+ elementRef(el);
2392
+ setConfigVarsRef(el);
2393
+ },
2394
+ className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
2395
+ role: "region",
2396
+ "aria-label": bundle.title,
2397
+ children: [
2398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "lb-bundle-header", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle-header__content", children: [
2399
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "lb-bundle-title", children: bundle.title }),
2400
+ bundle.description && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "lb-bundle-subtitle", children: bundle.description })
2401
+ ] }) }),
2402
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle-product-row lb-bundle-product-row--volume", children: [
2403
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2404
+ "img",
2405
+ {
2406
+ src: thumbImage.url,
2407
+ alt: thumbImage.altText ?? product.title,
2408
+ className: "lb-bundle__product-image",
2409
+ loading: "lazy"
2410
+ }
2411
+ ),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle__product-info", children: [
2413
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
2414
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "lb-bundle__product-price", children: [
2415
+ (0, import_core12.formatMoney)(basePrice, currency),
2416
+ " each"
2417
+ ] }),
2418
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
2419
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
2420
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
2421
+ value: o.value,
2422
+ label: o.value,
2423
+ disabled: o.disabled
2424
+ }));
2425
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2426
+ VariantDropdown,
2427
+ {
2428
+ options: dropdownOptions,
2429
+ value: selectedValues[optionIndex] ?? null,
2430
+ onChange: (v) => setOptionValue(optionIndex, v),
2431
+ ariaLabel: optionName
2432
+ },
2433
+ optionName
2434
+ );
2435
+ }) })
2436
+ ] })
2437
+ ] }),
2438
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2439
+ "div",
2440
+ {
2441
+ className: "lb-bundle__tiers lb-edge-fade",
2442
+ role: "table",
2443
+ "aria-label": "Volume discounts",
2444
+ ref: tiersEdgeFadeRef,
2445
+ children: tierSavings.map((ts, tierIndex) => {
2446
+ const savingsPercent = Math.round(ts.savingsPercent);
2447
+ const showCompare = bundle.widgetConfig.pricing.showComparePrice && ts.savings > 0;
2448
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2449
+ "div",
2450
+ {
2451
+ className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
2452
+ role: "row",
2453
+ children: [
2454
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle__tier-radio", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle__tier-radio-dot" }) }),
2455
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle__tier-info", role: "cell", children: [
2456
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle__tier-label", children: [
2457
+ "Buy ",
2458
+ ts.tier.minQuantity
2459
+ ] }),
2460
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle__tier-price", children: [
2461
+ showCompare && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle__tier-compare", children: (0, import_core12.formatMoney)(basePrice, currency) }),
2462
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: (0, import_core12.formatMoney)(ts.unitPrice, currency) }),
2463
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle__tier-unit", children: "each" })
2464
+ ] })
2465
+ ] }),
2466
+ (tierIndex === popularTierIndex || savingsPercent > 0) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle__tier-right", role: "cell", children: [
2467
+ tierIndex === popularTierIndex && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle__tier-badge", children: bundle.widgetConfig.popularBadge.text }),
2468
+ savingsPercent > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle__tier-savings", children: [
2469
+ "Save ",
2470
+ savingsPercent,
2471
+ "%"
2472
+ ] })
2473
+ ] })
2474
+ ]
2475
+ },
2476
+ ts.tier.minQuantity
2477
+ );
2478
+ })
2479
+ }
2480
+ ),
2481
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
2482
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
2483
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
2484
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2485
+ "button",
2486
+ {
2487
+ "aria-label": "Decrease quantity",
2488
+ onClick: () => setQuantity((q) => Math.max(1, q - 1)),
2489
+ children: "\u2212"
2490
+ }
2491
+ ),
2492
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2493
+ "input",
2494
+ {
2495
+ id: `lb-qty-${bundle.id}`,
2496
+ type: "number",
2497
+ min: 1,
2498
+ value: quantity,
2499
+ onChange: (e) => {
2500
+ const val = parseInt(e.target.value, 10);
2501
+ if (!isNaN(val) && val > 0) setQuantity(val);
2502
+ },
2503
+ className: "lb-bundle__quantity-input"
2504
+ }
2505
+ ),
2506
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2507
+ "button",
2508
+ {
2509
+ "aria-label": "Increase quantity",
2510
+ onClick: () => setQuantity((q) => q + 1),
2511
+ children: "+"
2512
+ }
2513
+ )
2514
+ ] })
2515
+ ] }),
2516
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
2517
+ bundle && displayVariant && (0, import_core12.shouldShowLowStockBadge)(
2518
+ displayVariant,
2519
+ minTierQty,
2520
+ bundle.widgetConfig.lowStockThreshold,
2521
+ bundle.widgetConfig.showLowStockBadge
2522
+ ) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
2523
+ "Only ",
2524
+ displayVariant.quantityAvailable,
2525
+ " left"
2526
+ ] }),
2527
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "lb-bundle-divider" }),
2528
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle-summary", children: [
2529
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "lb-bundle-summary__text", children: [
2530
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle-summary__label", "data-total-label": true, children: [
2531
+ "Total",
2532
+ bundle.widgetConfig.pricing.showItemCount && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { "data-item-count": true, children: [
2533
+ " ",
2534
+ "(",
2535
+ quantity,
2536
+ " item",
2537
+ quantity === 1 ? "" : "s",
2538
+ ")"
2539
+ ] })
2540
+ ] }),
2541
+ bundle.widgetConfig.savingsBar.visible && volumeSavings > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
2542
+ "You save",
2543
+ " ",
2544
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "data-savings-amount": true, children: (0, import_core12.formatMoney)(volumeSavings, currency) }),
2545
+ " ",
2546
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { "data-savings-percent": true, children: [
2547
+ "(",
2548
+ volumeSavingsPercent,
2549
+ "%)"
2550
+ ] })
2551
+ ] })
2552
+ ] }),
2553
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "lb-bundle-summary__prices", children: [
2554
+ bundle.widgetConfig.pricing.showComparePrice && volumeSavings > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: (0, import_core12.formatMoney)(undiscountedTotal, currency) }),
2555
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "lb-bundle-sale-price", "data-total-price": true, children: (0, import_core12.formatMoney)(volumeTotal, currency) })
2556
+ ] })
2557
+ ] }),
2558
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2559
+ "button",
2560
+ {
2561
+ className: "lb-bundle__cta",
2562
+ onClick: handleAddToCart,
2563
+ disabled: addingToCart || isSoldOut,
2564
+ "aria-busy": addingToCart,
2565
+ children: addingToCart ? "Adding..." : isSoldOut ? "Sold out" : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
2566
+ }
2567
+ )
2568
+ ]
2569
+ }
2570
+ );
2571
+ }
2572
+
2573
+ // src/components/BogoBundle.tsx
2574
+ var import_react14 = require("react");
2575
+ var import_core13 = require("@lime-bundles/core");
2576
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2577
+ function discountedUnit(price, percent) {
2578
+ const cents = Math.round(price * 100);
2579
+ const discounted = cents - Math.floor(cents * percent / 100);
2580
+ return discounted / 100;
2581
+ }
2582
+ function BogoBundle(props) {
2583
+ const {
2584
+ shopDomain,
2585
+ storefrontAccessToken,
2586
+ bundleGid,
2587
+ appUrl,
2588
+ analyticsEnabled,
2589
+ onAddToCart,
2590
+ onError,
2591
+ className
2592
+ } = props;
2593
+ const result = useBundleData({
2594
+ shopDomain,
2595
+ storefrontAccessToken,
2596
+ bundleGid
2597
+ });
2598
+ const { elementRef, trackAddToCart } = useAnalytics({
2599
+ shopDomain,
2600
+ appUrl: appUrl ?? `https://${shopDomain}`,
2601
+ bundleGid,
2602
+ bundleType: "bogo",
2603
+ enabled: analyticsEnabled !== false
2604
+ });
2605
+ const [addingToCart, setAddingToCart] = (0, import_react14.useState)(false);
2606
+ const [cartError, setCartError] = (0, import_react14.useState)(null);
2607
+ const [selectedVariants, setSelectedVariants] = (0, import_react14.useState)({ buy: null, get: null });
2608
+ const handleVariantChange = (0, import_react14.useCallback)(
2609
+ (role, variant) => {
2610
+ setSelectedVariants((prev) => {
2611
+ if (prev[role] === variant) return prev;
2612
+ return { ...prev, [role]: variant };
2613
+ });
2614
+ },
2615
+ []
2616
+ );
2617
+ const bundle = result.status === "success" && result.bundle.bundleType === "bogo" ? result.bundle : null;
2618
+ const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
2619
+ (0, import_react14.useEffect)(() => {
2620
+ if (result.status === "error") {
2621
+ onError?.(result.error);
2622
+ return;
2623
+ }
2624
+ if (result.status === "success" && result.bundle.bundleType !== "bogo") {
2625
+ onError?.(
2626
+ new Error(
2627
+ `BogoBundle: expected bundleType="bogo", got "${result.bundle.bundleType}"`
2628
+ )
2629
+ );
2630
+ }
2631
+ }, [result, onError]);
2632
+ const buyProduct = (0, import_react14.useMemo)(
2633
+ () => bundle?.products.find((p) => p.id === bundle.buyProductId) ?? null,
2634
+ [bundle]
2635
+ );
2636
+ const getProduct = (0, import_react14.useMemo)(
2637
+ () => bundle?.products.find((p) => p.id === bundle.getProductId) ?? null,
2638
+ [bundle]
2639
+ );
2640
+ const percent = bundle ? Math.min(100, Math.max(0, bundle.discountConfig.discountValue)) : 0;
2641
+ const resolveVariant = (0, import_react14.useCallback)(
2642
+ (product, selected, qty) => {
2643
+ if (!product) return null;
2644
+ return selected ?? product.variants.nodes.find((v) => (0, import_core13.isVariantFulfillable)(v, qty)) ?? product.variants.nodes[0] ?? null;
2645
+ },
2646
+ []
2647
+ );
2648
+ const oosSides = (0, import_react14.useMemo)(() => {
2649
+ if (!bundle) return 0;
2650
+ const short = (product, qty) => !!product && !product.variants.nodes.some((v) => (0, import_core13.isVariantFulfillable)(v, qty));
2651
+ return (short(buyProduct, bundle.buyQuantity) ? 1 : 0) + (short(getProduct, bundle.getQuantity) ? 1 : 0);
2652
+ }, [bundle, buyProduct, getProduct]);
2653
+ const handleAddToCart = (0, import_react14.useCallback)(async () => {
2654
+ if (!bundle || !buyProduct || !getProduct) return;
2655
+ const buyVariant2 = resolveVariant(buyProduct, selectedVariants.buy, bundle.buyQuantity);
2656
+ const getVariant2 = resolveVariant(getProduct, selectedVariants.get, bundle.getQuantity);
2657
+ if (!buyVariant2 || !getVariant2) return;
2658
+ const attributes = [
2659
+ { key: "_lime_bundle_gid", value: bundle.id },
2660
+ { key: "_lime_bundle_type", value: bundle.bundleType }
2661
+ ];
2662
+ const lines = buyVariant2.id === getVariant2.id ? [
2663
+ {
2664
+ merchandiseId: buyVariant2.id,
2665
+ quantity: bundle.buyQuantity + bundle.getQuantity,
2666
+ attributes
2667
+ }
2668
+ ] : [
2669
+ {
2670
+ merchandiseId: buyVariant2.id,
2671
+ quantity: bundle.buyQuantity,
2672
+ attributes
2673
+ },
2674
+ {
2675
+ merchandiseId: getVariant2.id,
2676
+ quantity: bundle.getQuantity,
2677
+ attributes
2678
+ }
2679
+ ];
2680
+ setAddingToCart(true);
2681
+ setCartError(null);
2682
+ try {
2683
+ await onAddToCart(lines);
2684
+ const buyUnit2 = parseFloat(buyVariant2.price.amount);
2685
+ const getUnit2 = parseFloat(getVariant2.price.amount);
2686
+ const totalPrice = buyUnit2 * bundle.buyQuantity + discountedUnit(getUnit2, percent) * bundle.getQuantity;
2687
+ trackAddToCart({
2688
+ quantity: bundle.buyQuantity + bundle.getQuantity,
2689
+ totalPrice: Math.round(totalPrice * 100) / 100
2690
+ });
2691
+ } catch (err) {
2692
+ const error = err instanceof Error ? err : new Error(String(err));
2693
+ setCartError(error.message);
2694
+ onError?.(error);
2695
+ } finally {
2696
+ setAddingToCart(false);
2697
+ }
2698
+ }, [bundle, buyProduct, getProduct, selectedVariants, resolveVariant, onAddToCart, onError, trackAddToCart, percent]);
2699
+ if (result.status === "loading") {
2700
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
2701
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
2702
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
2703
+ ] });
2704
+ }
2705
+ if (result.status === "error") return null;
2706
+ if (!bundle || !buyProduct || !getProduct) return null;
2707
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
2708
+ const buyVariant = resolveVariant(buyProduct, selectedVariants.buy, bundle.buyQuantity);
2709
+ const getVariant = resolveVariant(getProduct, selectedVariants.get, bundle.getQuantity);
2710
+ const buyUnit = buyVariant ? parseFloat(buyVariant.price.amount) : 0;
2711
+ const getUnit = getVariant ? parseFloat(getVariant.price.amount) : 0;
2712
+ const comparePrice = buyUnit * bundle.buyQuantity + getUnit * bundle.getQuantity;
2713
+ const salePrice = buyUnit * bundle.buyQuantity + discountedUnit(getUnit, percent) * bundle.getQuantity;
2714
+ const savings = Math.max(0, comparePrice - salePrice);
2715
+ const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
2716
+ const showCompare = bundle.widgetConfig.pricing.showComparePrice && savings > 0;
2717
+ const showSavings = bundle.widgetConfig.savingsBar.visible && savings > 0;
2718
+ const badge = percent === 100 ? "Free" : `${percent}% off`;
2719
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2720
+ "div",
2721
+ {
2722
+ ref: (el) => {
2723
+ elementRef(el);
2724
+ setConfigVarsRef(el);
2725
+ },
2726
+ className: `lb-bundle lb-bundle--bogo ${className ?? ""}`,
2727
+ role: "region",
2728
+ "aria-label": bundle.title,
2729
+ children: [
2730
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-bundle-header", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle-header__content", children: [
2731
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "lb-bundle-title", children: bundle.title }),
2732
+ bundle.description && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "lb-bundle-subtitle", children: bundle.description })
2733
+ ] }) }),
2734
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle__products", children: [
2735
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2736
+ BogoProductRow,
2737
+ {
2738
+ side: "buy",
2739
+ product: buyProduct,
2740
+ quantity: bundle.buyQuantity,
2741
+ percent,
2742
+ badge: null,
2743
+ currency,
2744
+ onVariantChange: handleVariantChange
2745
+ }
2746
+ ),
2747
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-bogo__plus", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "12", height: "12", viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
2748
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
2749
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
2750
+ ] }) }),
2751
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2752
+ BogoProductRow,
2753
+ {
2754
+ side: "get",
2755
+ product: getProduct,
2756
+ quantity: bundle.getQuantity,
2757
+ percent,
2758
+ badge,
2759
+ currency,
2760
+ onVariantChange: handleVariantChange
2761
+ }
2762
+ )
2763
+ ] }),
2764
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-bundle-divider" }),
2765
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle-summary", children: [
2766
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle-summary__text", children: [
2767
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bundle-summary__label", children: "Bundle price" }),
2768
+ showSavings && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
2769
+ "You save",
2770
+ " ",
2771
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { "data-savings-amount": true, children: (0, import_core13.formatMoney)(savings, currency) }),
2772
+ " ",
2773
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { "data-savings-percent": true, children: [
2774
+ "(",
2775
+ savingsPercent,
2776
+ "%)"
2777
+ ] })
2778
+ ] })
2779
+ ] }),
2780
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "lb-bundle-summary__prices", children: [
2781
+ showCompare && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: (0, import_core13.formatMoney)(comparePrice, currency) }),
2782
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: (0, import_core13.formatMoney)(salePrice, currency) })
2783
+ ] })
2784
+ ] }),
2785
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
2786
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2787
+ "button",
2788
+ {
2789
+ className: "lb-bundle__cta",
2790
+ onClick: handleAddToCart,
2791
+ disabled: addingToCart || oosSides > 0,
2792
+ "aria-busy": addingToCart,
2793
+ children: addingToCart ? "Adding..." : oosSides > 0 ? `${oosSides} item${oosSides === 1 ? "" : "s"} unavailable` : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
2794
+ }
2795
+ )
2796
+ ]
2797
+ }
2798
+ );
2799
+ }
2800
+ function BogoProductRow({ side, product, quantity, percent, badge, currency, onVariantChange }) {
2801
+ const variants = product.variants.nodes;
2802
+ const optionNames = (0, import_react14.useMemo)(() => {
2803
+ const first = variants[0];
2804
+ return first ? first.selectedOptions.map((o) => o.name) : [];
2805
+ }, [variants]);
2806
+ const showPicker = variants.length > 1 && optionNames.length > 0;
2807
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
2808
+ variants,
2809
+ optionNames
2810
+ });
2811
+ (0, import_react14.useEffect)(() => {
2812
+ onVariantChange(side, selectedVariant ?? null);
2813
+ }, [selectedVariant, side, onVariantChange]);
2814
+ const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core13.isVariantFulfillable)(v, quantity)) ?? variants[0];
2815
+ const unit = displayVariant ? parseFloat(displayVariant.price.amount) : 0;
2816
+ const isGet = side === "get" && percent > 0;
2817
+ const priceText = isGet ? (0, import_core13.formatMoney)(discountedUnit(unit, percent), currency) : (0, import_core13.formatMoney)(unit, currency);
2818
+ const compareAt = isGet ? (0, import_core13.formatMoney)(unit, currency) : displayVariant?.compareAtPrice && parseFloat(displayVariant.compareAtPrice.amount) > unit ? (0, import_core13.formatMoney)(displayVariant.compareAtPrice.amount, currency) : null;
2819
+ const unitPriceText = displayVariant ? (0, import_core13.formatUnitPrice)(
2820
+ displayVariant.unitPrice,
2821
+ displayVariant.unitPriceMeasurement,
2822
+ currency
2823
+ ) : null;
2824
+ const thumbImage = displayVariant?.image ?? product.featuredImage;
2825
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle-product-row", part: "product", "data-bogo-role": side, children: [
2826
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2827
+ "img",
2828
+ {
2829
+ src: thumbImage.url,
2830
+ alt: thumbImage.altText ?? product.title,
2831
+ className: "lb-bundle__product-image",
2832
+ loading: "lazy"
2833
+ }
2834
+ ),
2835
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "lb-bundle__product-info", children: [
2836
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
2837
+ !showPicker && displayVariant && variants.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bundle-variant-badge", children: displayVariant.title }),
2838
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "lb-bundle-product-prices", children: [
2839
+ compareAt && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2840
+ "span",
2841
+ {
2842
+ className: "lb-bundle-product-compare-price",
2843
+ "data-product-compare-price": true,
2844
+ children: compareAt
2845
+ }
2846
+ ),
2847
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bundle__product-price", "data-product-price": true, children: priceText }),
2848
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "lb-bundle-qty-inline", "data-qty-inline": true, children: [
2849
+ "\xD7",
2850
+ quantity
2851
+ ] })
2852
+ ] }),
2853
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2854
+ "span",
2855
+ {
2856
+ className: "lb-bundle__product-unit-price",
2857
+ "data-product-unit-price": true,
2858
+ children: unitPriceText
2859
+ }
2860
+ ),
2861
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
2862
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
2863
+ value: o.value,
2864
+ label: o.value,
2865
+ disabled: o.disabled
2866
+ }));
2867
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2868
+ VariantDropdown,
2869
+ {
2870
+ options: dropdownOptions,
2871
+ value: selectedValues[optionIndex] ?? null,
2872
+ onChange: (v) => setOptionValue(optionIndex, v),
2873
+ ariaLabel: optionName
2874
+ },
2875
+ optionName
2876
+ );
2877
+ }) })
2878
+ ] }),
2879
+ badge && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "lb-bogo__badge", children: badge })
2880
+ ] });
2881
+ }
2882
+
2883
+ // src/components/MultiStepBundle.tsx
2884
+ var import_react16 = require("react");
2885
+ var import_core15 = require("@lime-bundles/core");
2886
+
2887
+ // src/components/MultiStepPicker.tsx
2888
+ var import_react15 = require("react");
2889
+ var import_react_dom2 = require("react-dom");
2890
+ var import_core14 = require("@lime-bundles/core");
2891
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2892
+ function ruleFor3(bundle, productId) {
2893
+ return bundle.productRules[productId] ?? import_core14.DEFAULT_PRODUCT_RULE;
2894
+ }
2895
+ function sanitizeId2(gid) {
2896
+ return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
2897
+ }
2898
+ function buildEligibleProducts2(bundle, stepIndex, oosBehavior) {
2899
+ const result = [];
2900
+ for (const product of (0, import_core14.productsForStep)(bundle, stepIndex)) {
2901
+ const rule = ruleFor3(bundle, product.id);
2902
+ const available = product.variants.nodes.filter(
2903
+ (v) => (0, import_core14.isVariantFulfillable)(v, rule.min)
2904
+ );
2905
+ const isOos = available.length === 0;
2906
+ if (isOos && oosBehavior === "hide") continue;
2907
+ result.push({
2908
+ product,
2909
+ variants: product.variants.nodes,
2910
+ firstAvailableVariant: available[0] ?? null,
2911
+ isOos
2912
+ });
2913
+ }
2914
+ return result;
2915
+ }
2916
+ function normalizeText2(str) {
2917
+ return str.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
2918
+ }
2919
+ var FILTERS_ALL2 = "__all__";
2920
+ function MultiStepPicker({
2921
+ outOfStockBehavior,
2922
+ bundle,
2923
+ currency,
2924
+ initialStep,
2925
+ showStepper,
2926
+ selections,
2927
+ onAdd,
2928
+ onUpdateQuantity,
2929
+ onRemoveVariant,
2930
+ canRemoveVariant,
2931
+ swapUnitsFor,
2932
+ onSwap,
2933
+ onClose,
2934
+ styleVarsRef
2935
+ }) {
2936
+ const wc = bundle.widgetConfig;
2937
+ const steps = bundle.steps;
2938
+ const overlayRef = (0, import_react15.useRef)(null);
2939
+ const dialogRef = (0, import_react15.useRef)(null);
2940
+ const titleRef = (0, import_react15.useRef)(null);
2941
+ const mouseDownTarget = (0, import_react15.useRef)(null);
2942
+ const [open, setOpen] = (0, import_react15.useState)(false);
2943
+ const [stepIndex, setStepIndex] = (0, import_react15.useState)(
2944
+ () => Math.max(0, Math.min(steps.length - 1, initialStep))
2945
+ );
2946
+ const [query, setQuery] = (0, import_react15.useState)("");
2947
+ const [activeType, setActiveType] = (0, import_react15.useState)(FILTERS_ALL2);
2948
+ const [announcement, setAnnouncement] = (0, import_react15.useState)("");
2949
+ const step = steps[stepIndex];
2950
+ const stepMin = step?.minQuantity ?? 1;
2951
+ const stepPicks = selections[stepIndex] ?? [];
2952
+ const unitsInStep = stepPicks.reduce((sum, s) => sum + s.quantity, 0);
2953
+ const stepIsSatisfied = unitsInStep >= stepMin;
2954
+ const isLastStep = stepIndex === steps.length - 1;
2955
+ const shownUnits = Math.min(unitsInStep, stepMin);
2956
+ const eligible = (0, import_react15.useMemo)(
2957
+ () => buildEligibleProducts2(bundle, stepIndex, outOfStockBehavior),
2958
+ [bundle, stepIndex, outOfStockBehavior]
2959
+ );
2960
+ const productTypes = (0, import_react15.useMemo)(() => {
2961
+ const types = [];
2962
+ const seen = /* @__PURE__ */ new Set();
2963
+ for (const ep of eligible) {
2964
+ const ty = (ep.product.productType ?? "").trim();
2965
+ if (ty && !seen.has(ty)) {
2966
+ seen.add(ty);
2967
+ types.push(ty);
2968
+ }
2969
+ }
2970
+ return types;
2971
+ }, [eligible]);
2972
+ const showFilters = wc.mixMatchShowTypeFilters && productTypes.length >= 2;
2973
+ (0, import_react15.useEffect)(() => {
2974
+ const id = requestAnimationFrame(() => setOpen(true));
2975
+ return () => cancelAnimationFrame(id);
2976
+ }, []);
2977
+ useScrollLock(true);
2978
+ useFocusTrap({
2979
+ active: true,
2980
+ containerRef: dialogRef,
2981
+ onEscape: onClose
2982
+ });
2983
+ function goToStep(i) {
2984
+ const clamped = Math.max(0, Math.min(steps.length - 1, i));
2985
+ setStepIndex(clamped);
2986
+ setQuery("");
2987
+ setActiveType(FILTERS_ALL2);
2988
+ const target = steps[clamped];
2989
+ const targetPicks = selections[clamped] ?? [];
2990
+ const targetUnits = targetPicks.reduce((sum, s) => sum + s.quantity, 0);
2991
+ setAnnouncement(
2992
+ `Step ${clamped + 1} of ${steps.length}. ${target.name}. ${Math.min(targetUnits, target.minQuantity)} of ${target.minQuantity} added`
2993
+ );
2994
+ titleRef.current?.focus();
2995
+ }
2996
+ const subtitle = `Step ${stepIndex + 1} of ${steps.length} \xB7 ${step?.name ?? ""}`;
2997
+ const filtered = (0, import_react15.useMemo)(() => {
2998
+ const nq = normalizeText2(query.trim());
2999
+ return eligible.filter((ep) => {
3000
+ const matchesQuery = !nq || normalizeText2(ep.product.title).includes(nq);
3001
+ const matchesType = activeType === FILTERS_ALL2 || (ep.product.productType ?? "") === activeType;
3002
+ return matchesQuery && matchesType;
3003
+ });
3004
+ }, [eligible, query, activeType]);
3005
+ const showEmpty = filtered.length === 0 && query.trim().length > 0;
3006
+ function onOverlayMouseDown(e) {
3007
+ mouseDownTarget.current = e.target;
3008
+ }
3009
+ function onOverlayMouseUp(e) {
3010
+ if (e.target === overlayRef.current && mouseDownTarget.current === overlayRef.current) {
3011
+ onClose();
3012
+ }
3013
+ mouseDownTarget.current = null;
3014
+ }
3015
+ const titleId = `lb-modal-title-${sanitizeId2(bundle.id)}`;
3016
+ const modal = (
3017
+ // The overlay is a backdrop: click-outside-to-close is a mouse convenience;
3018
+ // keyboard users close via Escape (handled by useFocusTrap), and the dialog
3019
+ // inside carries the interactive role + focus trap. So the static-element
3020
+ // interaction lint does not apply to this backdrop.
3021
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
3022
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3023
+ "div",
3024
+ {
3025
+ ref: (el) => {
3026
+ overlayRef.current = el;
3027
+ styleVarsRef(el);
3028
+ },
3029
+ className: `lb-mix-match__modal-overlay${open ? " lb-mix-match__modal-overlay--open" : ""}`,
3030
+ "data-modal-overlay": true,
3031
+ "data-bundle-gid": bundle.id,
3032
+ onMouseDown: onOverlayMouseDown,
3033
+ onMouseUp: onOverlayMouseUp,
3034
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3035
+ "div",
3036
+ {
3037
+ ref: dialogRef,
3038
+ className: `lb-mix-match__modal${showFilters ? "" : " lb-mix-match__modal--filters-hidden"}`,
3039
+ role: "dialog",
3040
+ "aria-modal": "true",
3041
+ "aria-labelledby": titleId,
3042
+ tabIndex: -1,
3043
+ children: [
3044
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-header", children: [
3045
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-header-top", children: [
3046
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-heading", children: [
3047
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3048
+ "h4",
3049
+ {
3050
+ className: "lb-mix-match__modal-title",
3051
+ id: titleId,
3052
+ tabIndex: -1,
3053
+ ref: titleRef,
3054
+ children: "Add to your bundle"
3055
+ }
3056
+ ),
3057
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "lb-mix-match__modal-subtitle", "data-modal-subtitle": true, children: subtitle })
3058
+ ] }),
3059
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3060
+ "button",
3061
+ {
3062
+ type: "button",
3063
+ className: "lb-mix-match__modal-close",
3064
+ "data-modal-close": true,
3065
+ "aria-label": "Close",
3066
+ onClick: onClose,
3067
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseIcon3, {})
3068
+ }
3069
+ )
3070
+ ] }),
3071
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3072
+ "div",
3073
+ {
3074
+ className: "lb-mix-match__progress lb-mix-match__modal-progress",
3075
+ "data-progress": true,
3076
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3077
+ "div",
3078
+ {
3079
+ className: "lb-mix-match__progress-segments",
3080
+ "data-modal-step-segments": true,
3081
+ role: "progressbar",
3082
+ "aria-valuenow": shownUnits,
3083
+ "aria-valuemin": 0,
3084
+ "aria-valuemax": stepMin,
3085
+ children: Array.from({ length: stepMin }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3086
+ "span",
3087
+ {
3088
+ className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
3089
+ "data-progress-segment": true
3090
+ },
3091
+ i
3092
+ ))
3093
+ }
3094
+ )
3095
+ }
3096
+ )
3097
+ ] }),
3098
+ wc.showSearch && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-search", children: [
3099
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3100
+ "input",
3101
+ {
3102
+ type: "text",
3103
+ className: "lb-mix-match__modal-search-input",
3104
+ "data-modal-search": true,
3105
+ role: "searchbox",
3106
+ "aria-label": "Search products",
3107
+ placeholder: "Search products",
3108
+ autoComplete: "off",
3109
+ value: query,
3110
+ onChange: (e) => setQuery(e.target.value)
3111
+ }
3112
+ ),
3113
+ query.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3114
+ "button",
3115
+ {
3116
+ type: "button",
3117
+ className: "lb-mix-match__modal-search-clear",
3118
+ "data-modal-search-clear": true,
3119
+ "aria-label": "Clear search",
3120
+ onClick: () => setQuery(""),
3121
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SearchClearIcon2, {})
3122
+ }
3123
+ )
3124
+ ] }),
3125
+ showFilters && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3126
+ "div",
3127
+ {
3128
+ className: "lb-mix-match__filters",
3129
+ "data-modal-filters": true,
3130
+ role: "group",
3131
+ "aria-label": "Filter by product type",
3132
+ children: [FILTERS_ALL2, ...productTypes].map((value) => {
3133
+ const isActive = value === activeType;
3134
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3135
+ "button",
3136
+ {
3137
+ type: "button",
3138
+ className: `lb-mix-match__filter${isActive ? " lb-mix-match__filter--active" : ""}`,
3139
+ "data-filter": value,
3140
+ "aria-pressed": isActive,
3141
+ onClick: () => setActiveType(value),
3142
+ children: value === FILTERS_ALL2 ? "All" : value
3143
+ },
3144
+ value
3145
+ );
3146
+ })
3147
+ }
3148
+ ),
3149
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, children: filtered.map((ep) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3150
+ MultiStepPickerRow,
3151
+ {
3152
+ bundle,
3153
+ eligible: ep,
3154
+ currency,
3155
+ showStepper,
3156
+ stepIndex,
3157
+ selections,
3158
+ onAdd,
3159
+ swapUnitsFor,
3160
+ onSwap,
3161
+ onUpdateQuantity,
3162
+ onRemoveVariant,
3163
+ canRemoveVariant
3164
+ },
3165
+ `${stepIndex}:${ep.product.id}`
3166
+ )) }),
3167
+ showEmpty && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "lb-mix-match__modal-empty", "data-modal-empty": true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: "No products match your search" }) }),
3168
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-modal-live": true, "aria-live": "polite", className: "lb-visually-hidden", children: announcement || (query.trim() ? `${filtered.length} products shown` : "") }),
3169
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-footer lb-multi-step__modal-footer", children: [
3170
+ stepIndex > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3171
+ "button",
3172
+ {
3173
+ type: "button",
3174
+ className: "lb-multi-step__modal-back",
3175
+ "data-modal-back": true,
3176
+ onClick: () => goToStep(stepIndex - 1),
3177
+ children: "Back"
3178
+ }
3179
+ ),
3180
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3181
+ "span",
3182
+ {
3183
+ className: "lb-mix-match__modal-footer-count",
3184
+ "data-modal-footer-count": true,
3185
+ "aria-live": "polite",
3186
+ children: `${shownUnits} of ${stepMin} added`
3187
+ }
3188
+ ),
3189
+ !isLastStep && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3190
+ "button",
3191
+ {
3192
+ type: "button",
3193
+ className: "lb-mix-match__modal-done",
3194
+ "data-modal-next": true,
3195
+ disabled: !stepIsSatisfied,
3196
+ onClick: () => {
3197
+ if (stepIsSatisfied) goToStep(stepIndex + 1);
3198
+ },
3199
+ children: "Next"
3200
+ }
3201
+ ),
3202
+ isLastStep && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3203
+ "button",
3204
+ {
3205
+ type: "button",
3206
+ className: "lb-mix-match__modal-done",
3207
+ "data-modal-done": true,
3208
+ disabled: !stepIsSatisfied,
3209
+ onClick: onClose,
3210
+ children: "Done"
3211
+ }
3212
+ )
3213
+ ] })
3214
+ ]
3215
+ }
3216
+ )
3217
+ }
3218
+ )
3219
+ );
3220
+ if (typeof document === "undefined") return null;
3221
+ return (0, import_react_dom2.createPortal)(modal, document.body);
3222
+ }
3223
+ function MultiStepPickerRow({
3224
+ bundle,
3225
+ eligible,
3226
+ currency,
3227
+ showStepper,
3228
+ stepIndex,
3229
+ selections,
3230
+ onAdd,
3231
+ onUpdateQuantity,
3232
+ onRemoveVariant,
3233
+ canRemoveVariant,
3234
+ swapUnitsFor,
3235
+ onSwap
3236
+ }) {
3237
+ const { product, variants, isOos } = eligible;
3238
+ const rule = ruleFor3(bundle, product.id);
3239
+ const step = bundle.steps[stepIndex];
3240
+ const optionNames = (0, import_react15.useMemo)(() => {
3241
+ const first = variants[0];
3242
+ return first ? first.selectedOptions.map((o) => o.name) : [];
3243
+ }, [variants]);
3244
+ const showPicker = variants.length > 1 && optionNames.length > 0;
3245
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
3246
+ const currentVariant = selectedVariant ?? variants.find((v) => (0, import_core14.isVariantFulfillable)(v, rule.min)) ?? eligible.firstAvailableVariant ?? variants[0] ?? null;
3247
+ const stepPicks = (0, import_react15.useMemo)(
3248
+ () => selections[stepIndex] ?? [],
3249
+ [selections, stepIndex]
3250
+ );
3251
+ const unitsInStep = stepPicks.reduce((sum, s) => sum + s.quantity, 0);
3252
+ const committedSelection = (0, import_react15.useMemo)(
3253
+ () => currentVariant ? stepPicks.find((s) => s.variantId === currentVariant.id) ?? null : null,
3254
+ [stepPicks, currentVariant]
3255
+ );
3256
+ const variantInStep = committedSelection !== null;
3257
+ const committedQty = committedSelection?.quantity ?? 0;
3258
+ const productOtherUnits = (0, import_react15.useMemo)(() => {
3259
+ if (!currentVariant) return 0;
3260
+ let sum = 0;
3261
+ for (let i = 0; i < selections.length; i++) {
3262
+ for (const s of selections[i]) {
3263
+ if (s.productId !== product.id) continue;
3264
+ if (i === stepIndex && s.variantId === currentVariant.id) continue;
3265
+ sum += s.quantity;
3266
+ }
3267
+ }
3268
+ return sum;
3269
+ }, [selections, product.id, currentVariant, stepIndex]);
3270
+ const variantOtherUnits = (0, import_react15.useMemo)(() => {
3271
+ if (!currentVariant) return 0;
3272
+ let sum = 0;
3273
+ for (const picks of selections) {
3274
+ for (const s of picks) {
3275
+ if (s.variantId === currentVariant.id) sum += s.quantity;
3276
+ }
3277
+ }
3278
+ return Math.max(0, sum - committedQty);
3279
+ }, [selections, currentVariant, committedQty]);
3280
+ const swapUnits = currentVariant ? swapUnitsFor?.(stepIndex, product.id, currentVariant.id) ?? 0 : 0;
3281
+ const inSwapState = swapUnits >= rule.min;
3282
+ const room = (0, import_core14.stepHeadroom)(step?.maxQuantity ?? null, unitsInStep);
3283
+ const productHeadroom = rule.max - productOtherUnits;
3284
+ const spotCeiling = inSwapState ? swapUnits : Math.min(
3285
+ productHeadroom,
3286
+ room === Number.POSITIVE_INFINITY ? productHeadroom : committedQty + room
3287
+ );
3288
+ const stockRemaining = currentVariant ? (0, import_core14.maxAddableQuantity)(currentVariant, Number.MAX_SAFE_INTEGER, variantOtherUnits) : 0;
3289
+ const cap = Math.max(0, Math.min(spotCeiling, stockRemaining));
3290
+ const stepperMax = Math.max(rule.min, cap);
3291
+ const [qty, setQty] = (0, import_react15.useState)(rule.min);
3292
+ (0, import_react15.useEffect)(() => {
3293
+ setQty((prev) => Math.max(rule.min, Math.min(prev, stepperMax)));
3294
+ }, [stepperMax, rule.min]);
3295
+ const currentVariantId = currentVariant?.id ?? null;
3296
+ (0, import_react15.useEffect)(() => {
3297
+ if (!variantInStep) setQty(rule.min);
3298
+ }, [variantInStep, currentVariantId, rule.min]);
3299
+ if (!currentVariant || !step) return null;
3300
+ const thumbImage = currentVariant.image ?? product.featuredImage ?? null;
3301
+ const unitPriceText = (0, import_core14.formatUnitPrice)(
3302
+ currentVariant.unitPrice,
3303
+ currentVariant.unitPriceMeasurement,
3304
+ currency
3305
+ );
3306
+ const noStock = cap < rule.min;
3307
+ const needsMoreSpots = !variantInStep && !inSwapState && room !== Number.POSITIVE_INFINITY && rule.min > room;
3308
+ const addDisabled = !variantInStep && (inSwapState ? noStock : noStock || needsMoreSpots);
3309
+ const lockedRequired = variantInStep && currentVariant != null && !(canRemoveVariant?.(currentVariant.id) ?? true);
3310
+ const stepperDisabled = isOos || !variantInStep && addDisabled;
3311
+ const shownQty = variantInStep ? committedQty : qty;
3312
+ function handleAddOrRemove() {
3313
+ if (variantInStep && currentVariant) {
3314
+ onRemoveVariant(stepIndex, currentVariant.id);
3315
+ return;
3316
+ }
3317
+ if (!currentVariant || noStock) return;
3318
+ const pickedQty = showStepper ? qty : rule.min;
3319
+ const item = {
3320
+ productId: product.id,
3321
+ variantId: currentVariant.id,
3322
+ title: product.title,
3323
+ url: `/products/${product.handle}`,
3324
+ variantTitle: currentVariant.title,
3325
+ featuredImage: thumbImage?.url ?? null,
3326
+ price: parseFloat(currentVariant.price.amount),
3327
+ compareAtPrice: currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null,
3328
+ unitPrice: unitPriceText,
3329
+ quantity: pickedQty
3330
+ };
3331
+ if (inSwapState && onSwap) {
3332
+ onSwap(stepIndex, { ...item, quantity: Math.min(pickedQty, swapUnits) });
3333
+ return;
3334
+ }
3335
+ if (needsMoreSpots) return;
3336
+ onAdd(stepIndex, item);
3337
+ }
3338
+ const priceCents = parseFloat(currentVariant.price.amount);
3339
+ const compareCents = currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null;
3340
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3341
+ "div",
3342
+ {
3343
+ className: `lb-mix-match__modal-product${isOos ? " lb-mix-match__modal-product--sold-out" : ""}${variantInStep ? " lb-mix-match__modal-product--in-bundle" : ""}`,
3344
+ "data-product-item": true,
3345
+ "data-title": normalizeText2(product.title),
3346
+ "data-type": product.productType ?? "",
3347
+ "aria-disabled": isOos || void 0,
3348
+ children: [
3349
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-product-thumb", children: [
3350
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3351
+ "img",
3352
+ {
3353
+ src: thumbImage.url,
3354
+ alt: thumbImage.altText ?? product.title,
3355
+ loading: "lazy"
3356
+ }
3357
+ ),
3358
+ variantInStep && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lb-mix-match__modal-added-badge", children: "Added" })
3359
+ ] }),
3360
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-product-info", children: [
3361
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "lb-mix-match__modal-product-title", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3362
+ "a",
3363
+ {
3364
+ href: `/products/${product.handle}`,
3365
+ target: "_blank",
3366
+ rel: "noopener noreferrer",
3367
+ children: product.title
3368
+ }
3369
+ ) }),
3370
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { className: "lb-mix-match__modal-product-price", children: [
3371
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-row-price-sale": true, children: (0, import_core14.formatMoney)(priceCents, currency) }),
3372
+ compareCents !== null && compareCents > priceCents && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("s", { className: "lb-mix-match__modal-product-compare", "data-row-compare": true, children: (0, import_core14.formatMoney)(compareCents, currency) })
3373
+ ] }),
3374
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price", children: unitPriceText }),
3375
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "lb-bundle-variant-option-groups", children: optionNames.map((optionName, optionIndex) => {
3376
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
3377
+ value: o.value,
3378
+ label: o.value,
3379
+ disabled: o.disabled
3380
+ }));
3381
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3382
+ "div",
3383
+ {
3384
+ className: "lb-bundle-variant-option-group",
3385
+ children: [
3386
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lb-bundle-variant-option-label", children: optionName }),
3387
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3388
+ VariantDropdown,
3389
+ {
3390
+ className: "lb-mix-match__variant-select-dropdown",
3391
+ options: dropdownOptions,
3392
+ value: selectedValues[optionIndex] ?? null,
3393
+ onChange: (v) => setOptionValue(optionIndex, v),
3394
+ ariaLabel: optionName
3395
+ }
3396
+ )
3397
+ ]
3398
+ },
3399
+ optionName
3400
+ );
3401
+ }) }),
3402
+ !showPicker && variants.length === 1 && currentVariant.title !== "Default Title" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lb-mix-match__filled-variant", children: currentVariant.title }),
3403
+ !isOos && needsMoreSpots && room > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lb-mix-match__modal-needs-spots", children: rule.min === 1 ? "Needs 1 spot" : `Needs ${rule.min} spots` }),
3404
+ isOos ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lb-mix-match__modal-sold-out-label", children: "Sold out" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "lb-mix-match__modal-product-actions", children: [
3405
+ showStepper && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "lb-bundle-variant-option-group lb-mix-match__qty-stepper-group", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3406
+ "div",
3407
+ {
3408
+ className: "lb-mix-match__qty-stepper",
3409
+ role: "group",
3410
+ "aria-label": "Quantity",
3411
+ children: [
3412
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3413
+ "button",
3414
+ {
3415
+ type: "button",
3416
+ className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus",
3417
+ "aria-label": "Decrease quantity",
3418
+ disabled: stepperDisabled || shownQty <= rule.min,
3419
+ onClick: () => {
3420
+ if (variantInStep && currentVariant) {
3421
+ onUpdateQuantity(
3422
+ stepIndex,
3423
+ product.id,
3424
+ currentVariant.id,
3425
+ Math.max(rule.min, committedQty - 1)
3426
+ );
3427
+ } else {
3428
+ setQty((q) => Math.max(rule.min, q - 1));
3429
+ }
3430
+ },
3431
+ children: "\u2212"
3432
+ }
3433
+ ),
3434
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3435
+ "span",
3436
+ {
3437
+ className: "lb-mix-match__qty-stepper-value",
3438
+ "data-row-qty": true,
3439
+ "aria-live": "polite",
3440
+ children: shownQty
3441
+ }
3442
+ ),
3443
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3444
+ "button",
3445
+ {
3446
+ type: "button",
3447
+ className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus",
3448
+ "aria-label": "Increase quantity",
3449
+ disabled: stepperDisabled || shownQty >= stepperMax,
3450
+ onClick: () => {
3451
+ if (variantInStep && currentVariant) {
3452
+ onUpdateQuantity(
3453
+ stepIndex,
3454
+ product.id,
3455
+ currentVariant.id,
3456
+ Math.min(stepperMax, committedQty + 1)
3457
+ );
3458
+ } else {
3459
+ setQty((q) => Math.min(stepperMax, q + 1));
3460
+ }
3461
+ },
3462
+ children: "+"
3463
+ }
3464
+ )
3465
+ ]
3466
+ }
3467
+ ) }),
3468
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3469
+ "button",
3470
+ {
3471
+ type: "button",
3472
+ className: `lb-mix-match__modal-add${variantInStep ? " lb-mix-match__modal-add--added" : ""}${lockedRequired ? " lb-mix-match__modal-add--required" : ""}`,
3473
+ "data-add-product": true,
3474
+ "aria-label": variantInStep ? lockedRequired ? `${product.title} is required and cannot be removed` : `Remove ${product.title}` : `${inSwapState ? "Swap" : "Add"} ${product.title}`,
3475
+ disabled: addDisabled || lockedRequired,
3476
+ onClick: handleAddOrRemove,
3477
+ children: variantInStep ? lockedRequired ? "Required" : "Remove" : inSwapState ? "Swap" : "Add"
3478
+ }
3479
+ )
3480
+ ] })
3481
+ ] })
3482
+ ]
3483
+ }
3484
+ );
3485
+ }
3486
+ function CloseIcon3() {
3487
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3488
+ "svg",
3489
+ {
3490
+ width: "20",
3491
+ height: "20",
3492
+ viewBox: "0 0 20 20",
3493
+ fill: "none",
3494
+ xmlns: "http://www.w3.org/2000/svg",
3495
+ "aria-hidden": "true",
3496
+ children: [
3497
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
3498
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
3499
+ ]
3500
+ }
3501
+ );
3502
+ }
3503
+ function SearchClearIcon2() {
3504
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3505
+ "svg",
3506
+ {
3507
+ width: "16",
3508
+ height: "16",
3509
+ viewBox: "0 0 20 20",
3510
+ fill: "currentColor",
3511
+ xmlns: "http://www.w3.org/2000/svg",
3512
+ "aria-hidden": "true",
3513
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z" })
3514
+ }
3515
+ );
3516
+ }
3517
+
3518
+ // src/components/MultiStepBundle.tsx
3519
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3520
+ function ruleFor4(bundle, productId) {
3521
+ return bundle.productRules[productId] ?? import_core15.DEFAULT_PRODUCT_RULE;
3522
+ }
3523
+ function canRemoveItem2(bundle, selections, item) {
3524
+ const rule = ruleFor4(bundle, item.productId);
3525
+ if (!rule.required) return true;
3526
+ let unitsElsewhere = 0;
3527
+ for (const stepPicks of selections) {
3528
+ for (const s of stepPicks) {
3529
+ if (s.productId === item.productId && s !== item) {
3530
+ unitsElsewhere += s.quantity;
3531
+ }
3532
+ }
3533
+ }
3534
+ return unitsElsewhere >= rule.min;
3535
+ }
3536
+ function swapUnitsForItem2(bundle, selections, step, productId, variantId) {
3537
+ const rule = ruleFor4(bundle, productId);
3538
+ if (!rule.required || rule.max !== rule.min) return 0;
3539
+ const stepPicks = selections[step] ?? [];
3540
+ if (stepPicks.some((s) => s.variantId === variantId)) return 0;
3541
+ return stepPicks.filter((s) => s.productId === productId && s.variantId !== variantId).reduce((sum, s) => sum + s.quantity, 0);
3542
+ }
3543
+ function MultiStepBundle(props) {
3544
+ const {
1226
3545
  shopDomain,
1227
3546
  storefrontAccessToken,
1228
3547
  bundleGid,
@@ -1237,85 +3556,239 @@ function VolumeBundle(props) {
1237
3556
  storefrontAccessToken,
1238
3557
  bundleGid
1239
3558
  });
3559
+ const { outOfStockBehavior } = useShopSettings({
3560
+ shopDomain,
3561
+ storefrontAccessToken
3562
+ });
1240
3563
  const { elementRef, trackAddToCart } = useAnalytics({
1241
3564
  shopDomain,
1242
3565
  appUrl: appUrl ?? `https://${shopDomain}`,
1243
3566
  bundleGid,
1244
- bundleType: "volume",
3567
+ bundleType: "multi_step",
1245
3568
  enabled: analyticsEnabled !== false
1246
3569
  });
1247
- const [quantity, setQuantity] = (0, import_react8.useState)(1);
1248
- const [addingToCart, setAddingToCart] = (0, import_react8.useState)(false);
1249
- const [cartError, setCartError] = (0, import_react8.useState)(null);
1250
- const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
3570
+ const [selections, setSelections] = (0, import_react16.useState)([]);
3571
+ const [pickerOpenAt, setPickerOpenAt] = (0, import_react16.useState)(null);
3572
+ const [addingToCart, setAddingToCart] = (0, import_react16.useState)(false);
3573
+ const [cartError, setCartError] = (0, import_react16.useState)(null);
3574
+ const bundle = result.status === "success" && result.bundle.bundleType === "multi_step" ? result.bundle : null;
1251
3575
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1252
- (0, import_react8.useEffect)(() => {
3576
+ const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
3577
+ (0, import_react16.useEffect)(() => {
1253
3578
  if (result.status === "error") {
1254
3579
  onError?.(result.error);
1255
3580
  return;
1256
3581
  }
1257
- if (result.status === "success" && result.bundle.bundleType !== "volume") {
3582
+ if (result.status === "success" && result.bundle.bundleType !== "multi_step") {
1258
3583
  onError?.(
1259
3584
  new Error(
1260
- `VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
3585
+ `MultiStepBundle: expected bundleType="multi_step", got "${result.bundle.bundleType}"`
1261
3586
  )
1262
3587
  );
1263
3588
  }
1264
3589
  }, [result, onError]);
1265
- const product = bundle?.products[0];
1266
- const variants = (0, import_react8.useMemo)(() => product?.variants.nodes ?? [], [product]);
1267
- const optionNames = (0, import_react8.useMemo)(() => {
1268
- const first = variants[0];
1269
- return first ? first.selectedOptions.map((o) => o.name) : [];
1270
- }, [variants]);
1271
- const showPicker = variants.length > 1 && optionNames.length > 0;
1272
- const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1273
- const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
1274
- const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core10.isVariantFulfillable)(v, minTierQty)) ?? variants[0];
1275
- const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1276
- const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
1277
- const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
1278
- const unitPriceText = displayVariant ? (0, import_core10.formatUnitPrice)(
1279
- displayVariant.unitPrice,
1280
- displayVariant.unitPriceMeasurement,
1281
- currency
1282
- ) : null;
1283
- const tierSavings = (0, import_react8.useMemo)(
1284
- () => bundle ? (0, import_core10.calculateTierSavings)(
1285
- bundle.volumeTiers,
1286
- basePrice,
1287
- quantity,
1288
- bundle.discountConfig.discountType
1289
- ) : [],
1290
- [bundle, basePrice, quantity]
3590
+ const steps = (0, import_react16.useMemo)(() => bundle?.steps ?? [], [bundle]);
3591
+ (0, import_react16.useEffect)(() => {
3592
+ const seeded = steps.map(() => []);
3593
+ if (bundle) {
3594
+ const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
3595
+ for (const [pid, rule] of Object.entries(bundle.productRules)) {
3596
+ if (!rule.required) continue;
3597
+ for (let i = 0; i < steps.length; i++) {
3598
+ const product = (0, import_core15.productsForStep)(bundle, i).find((p) => p.id === pid);
3599
+ const variant = product?.variants.nodes.find(
3600
+ (v) => (0, import_core15.isVariantFulfillable)(v, rule.min)
3601
+ );
3602
+ if (!product || !variant) continue;
3603
+ seeded[i].push({
3604
+ productId: product.id,
3605
+ variantId: variant.id,
3606
+ title: product.title,
3607
+ url: `/products/${product.handle}`,
3608
+ variantTitle: variant.title,
3609
+ featuredImage: variant.image?.url ?? product.featuredImage?.url ?? null,
3610
+ price: parseFloat(variant.price.amount),
3611
+ compareAtPrice: variant.compareAtPrice ? parseFloat(variant.compareAtPrice.amount) : null,
3612
+ unitPrice: (0, import_core15.formatUnitPrice)(
3613
+ variant.unitPrice,
3614
+ variant.unitPriceMeasurement,
3615
+ seedCurrency
3616
+ ),
3617
+ quantity: rule.min
3618
+ });
3619
+ break;
3620
+ }
3621
+ }
3622
+ }
3623
+ setSelections(seeded);
3624
+ }, [bundle, steps]);
3625
+ const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
3626
+ const totalMin = (0, import_react16.useMemo)(
3627
+ () => steps.reduce((sum, s) => sum + s.minQuantity, 0),
3628
+ [steps]
1291
3629
  );
1292
- const activeTier = bundle ? (0, import_core10.getActiveTier)(bundle.volumeTiers, quantity) : null;
1293
- const handleAddToCart = (0, import_react8.useCallback)(async () => {
1294
- if (!bundle || !product) return;
1295
- const variant = selectedVariant ?? product.variants.nodes.find(
1296
- (v) => (0, import_core10.isVariantFulfillable)(v, quantity)
1297
- );
1298
- if (!variant) return;
1299
- const lines = [
1300
- {
1301
- merchandiseId: variant.id,
1302
- quantity,
1303
- attributes: [
1304
- { key: "_lime_bundle_gid", value: bundle.id },
1305
- { key: "_lime_bundle_type", value: bundle.bundleType }
1306
- ]
3630
+ const unitsByStep = (0, import_react16.useMemo)(
3631
+ () => selections.map((picks) => picks.reduce((sum, s) => sum + s.quantity, 0)),
3632
+ [selections]
3633
+ );
3634
+ const totalQuantity = (0, import_react16.useMemo)(
3635
+ () => unitsByStep.reduce((sum, u) => sum + u, 0),
3636
+ [unitsByStep]
3637
+ );
3638
+ const stepSatisfied = (0, import_react16.useCallback)(
3639
+ (i) => (unitsByStep[i] ?? 0) >= (steps[i]?.minQuantity ?? 1),
3640
+ [unitsByStep, steps]
3641
+ );
3642
+ const allSatisfied = steps.length > 0 && steps.every((_, i) => stepSatisfied(i));
3643
+ const valid = !!bundle && allSatisfied;
3644
+ const completedSteps = steps.filter((_, i) => stepSatisfied(i)).length;
3645
+ const summary = (0, import_react16.useMemo)(() => {
3646
+ let comparePrice = 0;
3647
+ let salePrice = 0;
3648
+ for (const stepPicks of selections) {
3649
+ for (const sel of stepPicks) {
3650
+ if (sel.quantity <= 0) continue;
3651
+ comparePrice += sel.price * sel.quantity;
3652
+ if (bundle) {
3653
+ salePrice += (0, import_core15.calculateDiscount)(
3654
+ sel.price,
3655
+ bundle.discountConfig.discountType,
3656
+ bundle.discountConfig.discountValue
3657
+ ) * sel.quantity;
3658
+ }
1307
3659
  }
1308
- ];
3660
+ }
3661
+ const savings = Math.max(0, comparePrice - salePrice);
3662
+ const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
3663
+ return { comparePrice, salePrice, savings, savingsPercent };
3664
+ }, [selections, bundle]);
3665
+ const addSelection = (0, import_react16.useCallback)((step, item) => {
3666
+ setSelections((prev) => {
3667
+ if (step < 0 || step >= prev.length) return prev;
3668
+ if (prev[step].some((s) => s.variantId === item.variantId)) return prev;
3669
+ const next = prev.slice();
3670
+ next[step] = [...next[step], item];
3671
+ return next;
3672
+ });
3673
+ }, []);
3674
+ const swapSelection = (0, import_react16.useCallback)(
3675
+ (step, item) => {
3676
+ setSelections((prev) => {
3677
+ if (!bundle || step < 0 || step >= prev.length) return prev;
3678
+ const swapUnits = swapUnitsForItem2(
3679
+ bundle,
3680
+ prev,
3681
+ step,
3682
+ item.productId,
3683
+ item.variantId
3684
+ );
3685
+ if (swapUnits <= 0) return prev;
3686
+ const qty = Math.min(Math.max(1, item.quantity), swapUnits);
3687
+ let freed = 0;
3688
+ const stepPicks = [];
3689
+ for (const s of prev[step]) {
3690
+ if (freed < qty && s.productId === item.productId && s.variantId !== item.variantId) {
3691
+ const take = Math.min(s.quantity, qty - freed);
3692
+ freed += take;
3693
+ if (s.quantity > take) {
3694
+ stepPicks.push({ ...s, quantity: s.quantity - take });
3695
+ }
3696
+ } else {
3697
+ stepPicks.push(s);
3698
+ }
3699
+ }
3700
+ if (freed === 0) return prev;
3701
+ stepPicks.push({ ...item, quantity: freed });
3702
+ const next = prev.slice();
3703
+ next[step] = stepPicks;
3704
+ return next;
3705
+ });
3706
+ },
3707
+ [bundle]
3708
+ );
3709
+ const updateQuantity = (0, import_react16.useCallback)(
3710
+ (step, productId, variantId, quantity) => {
3711
+ setSelections((prev) => {
3712
+ if (step < 0 || step >= prev.length) return prev;
3713
+ const idx = prev[step].findIndex(
3714
+ (s) => s.productId === productId && s.variantId === variantId
3715
+ );
3716
+ if (idx === -1) return prev;
3717
+ const clamped = Math.max(1, quantity);
3718
+ if (clamped === prev[step][idx].quantity) return prev;
3719
+ const next = prev.slice();
3720
+ const stepPicks = next[step].slice();
3721
+ stepPicks[idx] = { ...stepPicks[idx], quantity: clamped };
3722
+ next[step] = stepPicks;
3723
+ return next;
3724
+ });
3725
+ },
3726
+ []
3727
+ );
3728
+ const removeVariant = (0, import_react16.useCallback)(
3729
+ (step, variantId) => {
3730
+ setSelections((prev) => {
3731
+ if (step < 0 || step >= prev.length) return prev;
3732
+ const idx = prev[step].findIndex((s) => s.variantId === variantId);
3733
+ if (idx === -1) return prev;
3734
+ if (bundle && !canRemoveItem2(bundle, prev, prev[step][idx])) return prev;
3735
+ const next = prev.slice();
3736
+ const stepPicks = next[step].slice();
3737
+ stepPicks.splice(idx, 1);
3738
+ next[step] = stepPicks;
3739
+ return next;
3740
+ });
3741
+ },
3742
+ [bundle]
3743
+ );
3744
+ const removeSlot = (0, import_react16.useCallback)(
3745
+ (step, index) => {
3746
+ setSelections((prev) => {
3747
+ if (step < 0 || step >= prev.length) return prev;
3748
+ if (index < 0 || index >= prev[step].length) return prev;
3749
+ if (bundle && !canRemoveItem2(bundle, prev, prev[step][index]))
3750
+ return prev;
3751
+ const next = prev.slice();
3752
+ const stepPicks = next[step].slice();
3753
+ stepPicks.splice(index, 1);
3754
+ next[step] = stepPicks;
3755
+ return next;
3756
+ });
3757
+ },
3758
+ [bundle]
3759
+ );
3760
+ const handleAddToCart = (0, import_react16.useCallback)(async () => {
3761
+ if (!bundle || !valid) return;
3762
+ const grouped = /* @__PURE__ */ new Map();
3763
+ for (const stepPicks of selections) {
3764
+ for (const sel of stepPicks) {
3765
+ if (sel.quantity <= 0) continue;
3766
+ const existing = grouped.get(sel.variantId);
3767
+ if (existing) existing.quantity += sel.quantity;
3768
+ else grouped.set(sel.variantId, { variantId: sel.variantId, quantity: sel.quantity });
3769
+ }
3770
+ }
3771
+ const lines = Array.from(grouped.values()).map((line) => ({
3772
+ merchandiseId: line.variantId,
3773
+ quantity: line.quantity,
3774
+ attributes: [
3775
+ { key: "_lime_bundle_gid", value: bundle.id },
3776
+ { key: "_lime_bundle_type", value: bundle.bundleType }
3777
+ ]
3778
+ }));
1309
3779
  setAddingToCart(true);
1310
3780
  setCartError(null);
1311
3781
  try {
1312
3782
  await onAddToCart(lines);
1313
- const unitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
3783
+ const totalPrice = selections.reduce(
3784
+ (sum, stepPicks) => sum + stepPicks.reduce((s, sel) => s + sel.price * sel.quantity, 0),
3785
+ 0
3786
+ );
1314
3787
  trackAddToCart({
1315
- productId: product.id,
1316
- quantity,
1317
- totalPrice: Math.round(unitPrice * quantity * 100) / 100
3788
+ quantity: totalQuantity,
3789
+ totalPrice: Math.round(totalPrice * 100) / 100
1318
3790
  });
3791
+ setSelections(steps.map(() => []));
1319
3792
  } catch (err) {
1320
3793
  const error = err instanceof Error ? err : new Error(String(err));
1321
3794
  setCartError(error.message);
@@ -1323,183 +3796,320 @@ function VolumeBundle(props) {
1323
3796
  } finally {
1324
3797
  setAddingToCart(false);
1325
3798
  }
1326
- }, [
1327
- bundle,
1328
- product,
1329
- quantity,
1330
- activeTier,
1331
- basePrice,
1332
- onAddToCart,
1333
- onError,
1334
- trackAddToCart,
1335
- selectedVariant,
1336
- tierSavings
1337
- ]);
3799
+ }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity, steps]);
1338
3800
  if (result.status === "loading") {
1339
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1340
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
1341
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
3801
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
3802
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
3803
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
1342
3804
  ] });
1343
3805
  }
1344
3806
  if (result.status === "error") return null;
1345
- if (!bundle) return null;
1346
- if (!product) return null;
1347
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3807
+ if (!bundle || steps.length === 0) return null;
3808
+ for (let i = 0; i < steps.length; i++) {
3809
+ if (!stepHasInStockProduct(bundle, i)) return null;
3810
+ }
3811
+ const requiredBlocked = Object.entries(bundle.productRules).some(
3812
+ ([pid, rule]) => {
3813
+ if (!rule.required) return false;
3814
+ return !steps.some(
3815
+ (_, i) => (0, import_core15.productsForStep)(bundle, i).some(
3816
+ (p) => p.id === pid && p.variants.nodes.some((v) => (0, import_core15.isVariantFulfillable)(v, rule.min))
3817
+ )
3818
+ );
3819
+ }
3820
+ );
3821
+ if (requiredBlocked) return null;
3822
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
3823
+ const ctaLabel = addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText || "Add to cart";
3824
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1348
3825
  "div",
1349
3826
  {
1350
3827
  ref: (el) => {
1351
3828
  elementRef(el);
1352
3829
  setConfigVarsRef(el);
1353
3830
  },
1354
- className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
3831
+ className: `lb-bundle lb-bundle--multi-step lb-mix-match lb-multi-step ${className ?? ""}`,
1355
3832
  role: "region",
1356
3833
  "aria-label": bundle.title,
3834
+ "data-required-quantity": totalMin,
1357
3835
  children: [
1358
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
1359
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
1360
- thumbImage && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1361
- "img",
3836
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-bundle-header", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-bundle-header__content", children: [
3837
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "lb-bundle-title", children: bundle.title }),
3838
+ bundle.description && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "lb-bundle-subtitle", children: bundle.description })
3839
+ ] }) }),
3840
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-mix-match__progress", "data-progress": true, children: [
3841
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3842
+ "div",
1362
3843
  {
1363
- src: thumbImage.url,
1364
- alt: thumbImage.altText ?? product.title,
1365
- className: "lb-bundle__product-image",
1366
- loading: "lazy"
1367
- }
1368
- ),
1369
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product-info", children: [
1370
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1371
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "lb-bundle__product-price", children: [
1372
- (0, import_core10.formatMoney)(basePrice, currency),
1373
- " each"
1374
- ] }),
1375
- unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1376
- showPicker && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1377
- const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1378
- value: o.value,
1379
- label: o.value,
1380
- disabled: o.disabled
1381
- }));
1382
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1383
- VariantDropdown,
3844
+ className: "lb-mix-match__progress-segments",
3845
+ role: "progressbar",
3846
+ "aria-valuenow": completedSteps,
3847
+ "aria-valuemin": 0,
3848
+ "aria-valuemax": steps.length,
3849
+ children: Array.from({ length: steps.length }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3850
+ "span",
1384
3851
  {
1385
- options: dropdownOptions,
1386
- value: selectedValues[optionIndex] ?? null,
1387
- onChange: (v) => setOptionValue(optionIndex, v),
1388
- ariaLabel: optionName
3852
+ className: `lb-mix-match__progress-segment${i < completedSteps ? " lb-mix-match__progress-segment--filled" : ""}`,
3853
+ "data-progress-segment": true
1389
3854
  },
1390
- optionName
1391
- );
1392
- }) })
1393
- ] })
1394
- ] }),
1395
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1396
- "div",
1397
- {
1398
- className: "lb-bundle__tiers",
1399
- role: "table",
1400
- "aria-label": "Volume discounts",
1401
- children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1402
- "div",
1403
- {
1404
- className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
1405
- role: "row",
1406
- children: [
1407
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
1408
- ts.tier.minQuantity,
1409
- "+ items"
1410
- ] }),
1411
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
1412
- (0, import_core10.formatMoney)(ts.unitPrice, currency),
1413
- " each"
1414
- ] }),
1415
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
1416
- "Save ",
1417
- ts.savingsPercent.toFixed(0),
1418
- "%"
1419
- ] })
1420
- ]
1421
- },
1422
- ts.tier.minQuantity
1423
- ))
1424
- }
1425
- ),
1426
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
1427
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
1428
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1429
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1430
- "button",
1431
- {
1432
- "aria-label": "Decrease quantity",
1433
- onClick: () => setQuantity((q) => Math.max(1, q - 1)),
1434
- children: "\u2212"
1435
- }
1436
- ),
1437
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1438
- "input",
3855
+ i
3856
+ ))
3857
+ }
3858
+ ),
3859
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-mix-match__progress-labels", children: [
3860
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3861
+ "span",
1439
3862
  {
1440
- id: `lb-qty-${bundle.id}`,
1441
- type: "number",
1442
- min: 1,
1443
- value: quantity,
1444
- onChange: (e) => {
1445
- const val = parseInt(e.target.value, 10);
1446
- if (!isNaN(val) && val > 0) setQuantity(val);
1447
- },
1448
- className: "lb-bundle__quantity-input"
3863
+ className: "lb-mix-match__progress-count",
3864
+ "data-progress-count": true,
3865
+ "aria-live": "polite",
3866
+ children: `${completedSteps} of ${steps.length} steps completed`
1449
3867
  }
1450
3868
  ),
1451
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1452
- "button",
3869
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3870
+ "span",
1453
3871
  {
1454
- "aria-label": "Increase quantity",
1455
- onClick: () => setQuantity((q) => q + 1),
1456
- children: "+"
3872
+ className: "lb-mix-match__progress-remaining",
3873
+ "data-progress-remaining": true,
3874
+ "aria-live": "polite",
3875
+ children: completedSteps < steps.length ? `${steps.length - completedSteps} more to go` : "Complete"
1457
3876
  }
1458
3877
  )
1459
3878
  ] })
1460
3879
  ] }),
1461
- cartError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1462
- bundle && displayVariant && (0, import_core10.shouldShowLowStockBadge)(
1463
- displayVariant,
1464
- minTierQty,
1465
- bundle.widgetConfig.lowStockThreshold,
1466
- bundle.widgetConfig.showLowStockBadge
1467
- ) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1468
- "Only ",
1469
- displayVariant.quantityAvailable,
1470
- " left"
3880
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-multi-step__groups", "data-step-groups": true, children: steps.map((step, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3881
+ "section",
3882
+ {
3883
+ className: "lb-multi-step__group",
3884
+ "data-step-group": i,
3885
+ children: [
3886
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-multi-step__group-heading", children: [
3887
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-multi-step__group-name", children: step.name }),
3888
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3889
+ "span",
3890
+ {
3891
+ className: `lb-multi-step__group-count${stepSatisfied(i) ? " lb-multi-step__group-count--met" : ""}`,
3892
+ "data-step-count": i,
3893
+ children: `${Math.min(unitsByStep[i] ?? 0, step.minQuantity)} of ${step.minQuantity} added`
3894
+ }
3895
+ )
3896
+ ] }),
3897
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3898
+ "div",
3899
+ {
3900
+ className: "lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots",
3901
+ "data-step-slots": i,
3902
+ children: (selections[i] ?? []).map((item, index) => (
3903
+ /* Whole card reopens the wizard AT this step. The inner
3904
+ title button provides the keyboard/AT path (its
3905
+ activation bubbles here); × stops propagation. */
3906
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- keyboard path is the inner "Edit selection" title button whose activation bubbles to this handler
3907
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3908
+ "div",
3909
+ {
3910
+ className: "lb-mix-match__slot lb-mix-match__slot--filled",
3911
+ onClick: () => setPickerOpenAt(i),
3912
+ children: [
3913
+ item.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-bundle-thumbnail", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3914
+ "img",
3915
+ {
3916
+ src: item.featuredImage,
3917
+ alt: item.title,
3918
+ loading: "lazy"
3919
+ }
3920
+ ) }),
3921
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-mix-match__filled-info", children: [
3922
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3923
+ "button",
3924
+ {
3925
+ type: "button",
3926
+ className: "lb-mix-match__filled-title",
3927
+ "aria-label": `Edit selection: ${item.title}`,
3928
+ children: item.title
3929
+ }
3930
+ ),
3931
+ item.variantTitle && item.variantTitle !== "Default Title" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-mix-match__filled-variant", children: item.variantTitle }),
3932
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "lb-mix-match__filled-price", children: [
3933
+ item.compareAtPrice !== null && item.compareAtPrice > item.price && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("s", { className: "lb-mix-match__filled-compare", children: (0, import_core15.formatMoney)(item.compareAtPrice, currency) }),
3934
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-product-price", children: (0, import_core15.formatMoney)(item.price, currency) }),
3935
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-qty-inline", children: `\xD7${item.quantity}` })
3936
+ ] }),
3937
+ item.unitPrice && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-product-unit-price", children: item.unitPrice })
3938
+ ] }),
3939
+ canRemoveItem2(bundle, selections, item) ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3940
+ "button",
3941
+ {
3942
+ type: "button",
3943
+ className: "lb-mix-match__slot-remove",
3944
+ "aria-label": `Remove ${item.title}`,
3945
+ onClick: (e) => {
3946
+ e.stopPropagation();
3947
+ removeSlot(i, index);
3948
+ },
3949
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CloseIcon4, {})
3950
+ }
3951
+ ) : (
3952
+ /* Required pick at its floor — quiet state chip instead
3953
+ of the ×; removal returns once another slot covers
3954
+ the product's minimum (variant swap flow). */
3955
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-mix-match__slot-required", children: "Required" })
3956
+ )
3957
+ ]
3958
+ },
3959
+ `${item.productId}:${item.variantId}:${index}`
3960
+ )
3961
+ ))
3962
+ }
3963
+ ),
3964
+ (0, import_core15.stepHeadroom)(step.maxQuantity, unitsByStep[i] ?? 0) !== 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3965
+ "button",
3966
+ {
3967
+ type: "button",
3968
+ className: "lb-mix-match__add-product lb-multi-step__step-trigger",
3969
+ "data-step-trigger": i,
3970
+ "aria-label": `${step.minQuantity === 1 && step.maxQuantity === 1 ? "Choose a product" : "Choose products"}: ${step.name}`,
3971
+ onClick: () => setPickerOpenAt(i),
3972
+ children: [
3973
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3974
+ "span",
3975
+ {
3976
+ className: "lb-mix-match__add-product-icon",
3977
+ "aria-hidden": "true",
3978
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlusIcon2, {})
3979
+ }
3980
+ ),
3981
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-mix-match__add-product-label", children: step.minQuantity === 1 && step.maxQuantity === 1 ? "Choose a product" : "Choose products" })
3982
+ ]
3983
+ }
3984
+ )
3985
+ ]
3986
+ },
3987
+ i
3988
+ )) }),
3989
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "lb-bundle-divider" }),
3990
+ allSatisfied && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-bundle-summary", "data-pricing-section": true, children: [
3991
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "lb-bundle-summary__text", children: [
3992
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-summary__label", children: "Bundle total" }),
3993
+ bundle.widgetConfig.savingsBar.visible && summary.savings > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
3994
+ "You save",
3995
+ " ",
3996
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "data-savings-amount": true, children: (0, import_core15.formatMoney)(summary.savings, currency) }),
3997
+ " ",
3998
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { "data-savings-percent": true, children: [
3999
+ "(",
4000
+ summary.savingsPercent,
4001
+ "%)"
4002
+ ] })
4003
+ ] })
4004
+ ] }),
4005
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "lb-bundle-summary__prices", children: [
4006
+ bundle.widgetConfig.pricing.showComparePrice && summary.savings > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: (0, import_core15.formatMoney)(summary.comparePrice, currency) }),
4007
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: (0, import_core15.formatMoney)(summary.salePrice, currency) })
4008
+ ] })
1471
4009
  ] }),
1472
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
4010
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
4011
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1473
4012
  "button",
1474
4013
  {
1475
4014
  className: "lb-bundle__cta",
1476
4015
  onClick: handleAddToCart,
1477
- disabled: addingToCart,
4016
+ disabled: addingToCart || !valid,
1478
4017
  "aria-busy": addingToCart,
1479
- children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
4018
+ children: ctaLabel
4019
+ }
4020
+ ),
4021
+ pickerOpenAt !== null && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4022
+ MultiStepPicker,
4023
+ {
4024
+ outOfStockBehavior,
4025
+ bundle,
4026
+ currency,
4027
+ initialStep: pickerOpenAt,
4028
+ showStepper,
4029
+ selections,
4030
+ onAdd: addSelection,
4031
+ onUpdateQuantity: updateQuantity,
4032
+ onRemoveVariant: removeVariant,
4033
+ canRemoveVariant: (variantId) => {
4034
+ for (const stepPicks of selections) {
4035
+ const item = stepPicks.find((p) => p.variantId === variantId);
4036
+ if (item) return canRemoveItem2(bundle, selections, item);
4037
+ }
4038
+ return true;
4039
+ },
4040
+ swapUnitsFor: (step, productId, variantId) => swapUnitsForItem2(bundle, selections, step, productId, variantId),
4041
+ onSwap: swapSelection,
4042
+ onClose: () => setPickerOpenAt(null),
4043
+ styleVarsRef: setModalConfigVarsRef
1480
4044
  }
1481
4045
  )
1482
4046
  ]
1483
4047
  }
1484
4048
  );
1485
4049
  }
4050
+ function stepHasInStockProduct(bundle, stepIndex) {
4051
+ return (0, import_core15.productsForStep)(bundle, stepIndex).some((product) => {
4052
+ const rule = ruleFor4(bundle, product.id);
4053
+ return product.variants.nodes.some((v) => (0, import_core15.isVariantFulfillable)(v, rule.min));
4054
+ });
4055
+ }
4056
+ function PlusIcon2() {
4057
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4058
+ "svg",
4059
+ {
4060
+ width: "18",
4061
+ height: "18",
4062
+ viewBox: "0 0 18 18",
4063
+ fill: "none",
4064
+ xmlns: "http://www.w3.org/2000/svg",
4065
+ "aria-hidden": "true",
4066
+ children: [
4067
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
4068
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
4069
+ ]
4070
+ }
4071
+ );
4072
+ }
4073
+ function CloseIcon4() {
4074
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4075
+ "svg",
4076
+ {
4077
+ width: "16",
4078
+ height: "16",
4079
+ viewBox: "0 0 20 20",
4080
+ fill: "none",
4081
+ xmlns: "http://www.w3.org/2000/svg",
4082
+ "aria-hidden": "true",
4083
+ children: [
4084
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
4085
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
4086
+ ]
4087
+ }
4088
+ );
4089
+ }
4090
+
4091
+ // src/fetchShopCustomCss.ts
4092
+ async function fetchShopCustomCss(options) {
4093
+ const settings = await fetchShopSettings(options);
4094
+ return settings.customCss;
4095
+ }
1486
4096
 
1487
4097
  // src/hooks/useBundlesForProduct.ts
1488
- var import_react9 = require("react");
1489
- var import_core11 = require("@lime-bundles/core");
4098
+ var import_react17 = require("react");
4099
+ var import_core16 = require("@lime-bundles/core");
1490
4100
  var INITIAL_STATE2 = {
1491
4101
  status: "loading",
1492
4102
  bundles: null,
1493
4103
  error: null
1494
4104
  };
1495
4105
  function useBundlesForProduct(options) {
1496
- const [state, setState] = (0, import_react9.useState)(
4106
+ const [state, setState] = (0, import_react17.useState)(
1497
4107
  INITIAL_STATE2
1498
4108
  );
1499
- (0, import_react9.useEffect)(() => {
4109
+ (0, import_react17.useEffect)(() => {
1500
4110
  const controller = new AbortController();
1501
4111
  setState(INITIAL_STATE2);
1502
- (0, import_core11.fetchBundlesForProduct)({
4112
+ (0, import_core16.fetchBundlesForProduct)({
1503
4113
  shopDomain: options.shopDomain,
1504
4114
  storefrontAccessToken: options.storefrontAccessToken,
1505
4115
  productHandle: options.productHandle,
@@ -1516,13 +4126,13 @@ function useBundlesForProduct(options) {
1516
4126
  error: err instanceof Error ? err : new Error(String(err))
1517
4127
  });
1518
4128
  });
1519
- fetchShopCustomCss({
4129
+ fetchShopSettings({
1520
4130
  shopDomain: options.shopDomain,
1521
4131
  storefrontAccessToken: options.storefrontAccessToken,
1522
4132
  signal: controller.signal
1523
- }).then((css) => {
4133
+ }).then((settings) => {
1524
4134
  if (controller.signal.aborted) return;
1525
- (0, import_core11.injectCustomCss)(options.shopDomain, css);
4135
+ (0, import_core16.injectCustomCss)(options.shopDomain, settings.customCss);
1526
4136
  }).catch(() => {
1527
4137
  });
1528
4138
  return () => {
@@ -1538,21 +4148,23 @@ function useBundlesForProduct(options) {
1538
4148
  }
1539
4149
 
1540
4150
  // src/index.ts
1541
- var import_core12 = require("@lime-bundles/core");
1542
- var import_core13 = require("@lime-bundles/core");
4151
+ var import_core17 = require("@lime-bundles/core");
4152
+ var import_core18 = require("@lime-bundles/core");
1543
4153
  // Annotate the CommonJS export names for ESM import in node:
1544
4154
  0 && (module.exports = {
1545
4155
  BUNDLES_FOR_PRODUCT_QUERY,
1546
4156
  BUNDLE_METAOBJECT_QUERY,
4157
+ BogoBundle,
1547
4158
  BundleParseError,
4159
+ DEFAULT_OUT_OF_STOCK_BEHAVIOR,
1548
4160
  FixedBundle,
1549
4161
  MixMatchBundle,
1550
- SHOP_CUSTOM_CSS_QUERY,
4162
+ MultiStepBundle,
4163
+ SHOP_SETTINGS_QUERY,
1551
4164
  StorefrontApiError,
1552
4165
  VariantDropdown,
1553
4166
  VolumeBundle,
1554
4167
  WIDGET_CONFIG_DEFAULTS,
1555
- applyABVariantB,
1556
4168
  applyWidgetConfigVars,
1557
4169
  calculateDiscount,
1558
4170
  calculateTierSavings,
@@ -1562,29 +4174,32 @@ var import_core13 = require("@lime-bundles/core");
1562
4174
  fetchBundleData,
1563
4175
  fetchBundlesForProduct,
1564
4176
  fetchShopCustomCss,
4177
+ fetchShopSettings,
4178
+ fetchShopSettingsOrDefault,
1565
4179
  formatCents,
1566
4180
  formatCountdown,
1567
4181
  formatMoney,
1568
4182
  formatUnitPrice,
1569
- getABTestAssignment,
1570
4183
  getActiveTier,
1571
- hasConsent,
1572
4184
  injectCustomCss,
1573
4185
  mergeWidgetConfig,
1574
4186
  observeImpression,
1575
4187
  parseCents,
1576
4188
  parseMetaobjectBundle,
1577
4189
  parseMetaobjectBundleStrict,
4190
+ parseOutOfStockBehavior,
1578
4191
  percentageDiscountUnit,
4192
+ productsForStep,
1579
4193
  reportAddToCart,
1580
4194
  reportImpression,
1581
4195
  sanitizeCustomCss,
1582
- setConsent,
4196
+ stepHeadroom,
1583
4197
  thumbnailRatioVars,
1584
4198
  transformImageUrl,
1585
4199
  useAnalytics,
1586
4200
  useBundleData,
1587
4201
  useBundlesForProduct,
4202
+ useShopSettings,
1588
4203
  useVariantSelection,
1589
4204
  useWidgetConfigVars,
1590
4205
  validateQuantity