@lime-bundles/react 5.0.0 → 6.1.0

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