@lime-bundles/react 4.1.0 → 6.0.0

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