@lime-bundles/react 5.0.0 → 6.0.0

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