@lime-bundles/react 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 () => {
@@ -192,8 +211,7 @@ function useAnalytics(options) {
192
211
  impressionFiredRef.current = true;
193
212
  reportImpression(config, {
194
213
  bundleGid: options.bundleGid,
195
- bundleType: options.bundleType,
196
- linkGroupId: options.linkGroupId
214
+ bundleType: options.bundleType
197
215
  });
198
216
  });
199
217
  return cleanup;
@@ -202,8 +220,7 @@ function useAnalytics(options) {
202
220
  config,
203
221
  options.enabled,
204
222
  options.bundleGid,
205
- options.bundleType,
206
- options.linkGroupId
223
+ options.bundleType
207
224
  ]);
208
225
  const trackAddToCart = useCallback(
209
226
  (event) => {
@@ -213,16 +230,14 @@ function useAnalytics(options) {
213
230
  bundleType: options.bundleType,
214
231
  productId: event.productId,
215
232
  quantity: event.quantity,
216
- totalPrice: event.totalPrice,
217
- linkGroupId: options.linkGroupId
233
+ totalPrice: event.totalPrice
218
234
  });
219
235
  },
220
236
  [
221
237
  config,
222
238
  options.enabled,
223
239
  options.bundleGid,
224
- options.bundleType,
225
- options.linkGroupId
240
+ options.bundleType
226
241
  ]
227
242
  );
228
243
  return { elementRef, trackAddToCart };
@@ -326,13 +341,58 @@ function useVariantSelection({
326
341
  };
327
342
  }
328
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
+
329
389
  // src/components/VariantDropdown.tsx
330
390
  import {
331
391
  useEffect as useEffect5,
332
392
  useId,
333
393
  useLayoutEffect,
334
394
  useMemo as useMemo3,
335
- useRef as useRef3,
395
+ useRef as useRef4,
336
396
  useState as useState4
337
397
  } from "react";
338
398
  import { dropdown } from "@lime-bundles/core";
@@ -363,9 +423,9 @@ function VariantDropdown({
363
423
  className
364
424
  }) {
365
425
  const idBase = useId();
366
- const triggerRef = useRef3(null);
367
- const listboxRef = useRef3(null);
368
- const typeAheadRef = useRef3(emptyTypeAheadState());
426
+ const triggerRef = useRef4(null);
427
+ const listboxRef = useRef4(null);
428
+ const typeAheadRef = useRef4(emptyTypeAheadState());
369
429
  const [isOpen, setIsOpen] = useState4(false);
370
430
  const [activeIndex, setActiveIndex] = useState4(-1);
371
431
  const [position, setPosition] = useState4(null);
@@ -627,7 +687,7 @@ function FixedBundle(props) {
627
687
  const [addingToCart, setAddingToCart] = useState5(false);
628
688
  const [cartError, setCartError] = useState5(null);
629
689
  const [selectedVariants, setSelectedVariants] = useState5({});
630
- const handleVariantChange = useCallback4(
690
+ const handleVariantChange = useCallback5(
631
691
  (productId, variant) => {
632
692
  setSelectedVariants((prev) => {
633
693
  if (prev[productId] === variant) return prev;
@@ -638,6 +698,7 @@ function FixedBundle(props) {
638
698
  );
639
699
  const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
640
700
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
701
+ const productsEdgeFadeRef = useEdgeFade();
641
702
  useEffect6(() => {
642
703
  if (result.status === "error") {
643
704
  onError?.(result.error);
@@ -651,7 +712,17 @@ function FixedBundle(props) {
651
712
  );
652
713
  }
653
714
  }, [result, onError]);
654
- 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 () => {
655
726
  if (!bundle) return;
656
727
  const bundleLines = bundle.products.filter(
657
728
  (p) => p.variants.nodes.some(
@@ -714,6 +785,27 @@ function FixedBundle(props) {
714
785
  if (result.status === "error") return null;
715
786
  if (!bundle) return null;
716
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;
717
809
  return /* @__PURE__ */ jsxs2(
718
810
  "div",
719
811
  {
@@ -725,34 +817,58 @@ function FixedBundle(props) {
725
817
  role: "region",
726
818
  "aria-label": bundle.title,
727
819
  children: [
728
- /* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
729
- bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
730
- /* @__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(
731
825
  FixedProductRow,
732
826
  {
733
827
  bundle,
734
828
  product,
735
829
  currency,
830
+ isOos: oosProductIds.has(product.id),
736
831
  onVariantChange: handleVariantChange
737
832
  },
738
833
  product.id
739
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
+ ] }),
740
856
  cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
741
857
  /* @__PURE__ */ jsx2(
742
858
  "button",
743
859
  {
744
860
  className: "lb-bundle__cta",
745
861
  onClick: handleAddToCart,
746
- disabled: addingToCart,
862
+ disabled: addingToCart || oosProductIds.size > 0,
747
863
  "aria-busy": addingToCart,
748
- 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"
749
865
  }
750
866
  )
751
867
  ]
752
868
  }
753
869
  );
754
870
  }
755
- function FixedProductRow({ bundle, product, currency, onVariantChange }) {
871
+ function FixedProductRow({ bundle, product, currency, isOos, onVariantChange }) {
756
872
  const variants = product.variants.nodes;
757
873
  const optionNames = useMemo4(() => {
758
874
  const first = variants[0];
@@ -770,11 +886,13 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
770
886
  (v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
771
887
  ) ?? variants[0];
772
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;
773
890
  const unitPriceText = displayVariant ? formatUnitPrice(
774
891
  displayVariant.unitPrice,
775
892
  displayVariant.unitPriceMeasurement,
776
893
  currency
777
894
  ) : null;
895
+ const displayQty = displayVariant ? resolveBundleQty(bundle, product.id, displayVariant.id) : 1;
778
896
  const thumbImage = displayVariant?.image ?? product.featuredImage;
779
897
  const showLowStock = !!displayVariant && shouldShowLowStockBadge(
780
898
  displayVariant,
@@ -782,304 +900,15 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
782
900
  bundle.widgetConfig.lowStockThreshold,
783
901
  bundle.widgetConfig.showLowStockBadge
784
902
  );
785
- return /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product", part: "product", children: [
786
- thumbImage && /* @__PURE__ */ jsx2(
787
- "img",
788
- {
789
- src: thumbImage.url,
790
- alt: thumbImage.altText ?? product.title,
791
- className: "lb-bundle__product-image",
792
- loading: "lazy"
793
- }
794
- ),
795
- /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
796
- /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
797
- /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
798
- unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
799
- showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
800
- "Only ",
801
- displayVariant.quantityAvailable,
802
- " left"
803
- ] }),
804
- showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
805
- const dropdownOptions = optionsFor(optionIndex).map((o) => ({
806
- value: o.value,
807
- label: o.value,
808
- disabled: o.disabled
809
- }));
810
- return /* @__PURE__ */ jsx2(
811
- VariantDropdown,
812
- {
813
- options: dropdownOptions,
814
- value: selectedValues[optionIndex] ?? null,
815
- onChange: (v) => setOptionValue(optionIndex, v),
816
- ariaLabel: optionName
817
- },
818
- optionName
819
- );
820
- }) })
821
- ] })
822
- ] });
823
- }
824
-
825
- // src/components/MixMatchBundle.tsx
826
- import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
827
- import {
828
- formatMoney as formatMoney2,
829
- formatUnitPrice as formatUnitPrice2,
830
- isVariantFulfillable as isVariantFulfillable2,
831
- shouldShowLowStockBadge as shouldShowLowStockBadge2,
832
- maxAddableQuantity,
833
- DEFAULT_PRODUCT_RULE
834
- } from "@lime-bundles/core";
835
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
836
- function ruleFor(bundle, productId) {
837
- return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
838
- }
839
- function MixMatchBundle(props) {
840
- const {
841
- shopDomain,
842
- storefrontAccessToken,
843
- bundleGid,
844
- appUrl,
845
- analyticsEnabled,
846
- onAddToCart,
847
- onError,
848
- className
849
- } = props;
850
- const result = useBundleData({
851
- shopDomain,
852
- storefrontAccessToken,
853
- bundleGid
854
- });
855
- const { elementRef, trackAddToCart } = useAnalytics({
856
- shopDomain,
857
- appUrl: appUrl ?? `https://${shopDomain}`,
858
- bundleGid,
859
- bundleType: "mix_match",
860
- enabled: analyticsEnabled !== false
861
- });
862
- const [selections, setSelections] = useState6(
863
- /* @__PURE__ */ new Map()
864
- );
865
- const [addingToCart, setAddingToCart] = useState6(false);
866
- const [cartError, setCartError] = useState6(null);
867
- const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
868
- const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
869
- useEffect7(() => {
870
- if (result.status === "error") {
871
- onError?.(result.error);
872
- return;
873
- }
874
- if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
875
- onError?.(
876
- new Error(
877
- `MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
878
- )
879
- );
880
- }
881
- }, [result, onError]);
882
- const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
883
- const distinctSelectedProducts = useMemo5(() => {
884
- const ids = /* @__PURE__ */ new Set();
885
- for (const sel of selections.values()) {
886
- if (sel.quantity > 0) ids.add(sel.productId);
887
- }
888
- return ids;
889
- }, [selections]);
890
- const totalQuantity = useMemo5(
891
- () => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
892
- [selections]
893
- );
894
- const requiredPicks = bundle?.minQuantity ?? 0;
895
- const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
896
- const valid = !!bundle && meetsMinPicks;
897
- const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
898
- const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
899
- const selectVariant = useCallback5(
900
- (productId, variant) => {
901
- if (!bundle) return;
902
- const rule = ruleFor(bundle, productId);
903
- const maxAddable = maxAddableQuantity(variant, rule.max, 0);
904
- if (maxAddable < rule.min) return;
905
- setSelections((prev) => {
906
- const next = new Map(prev);
907
- next.set(`${productId}:${variant.id}`, {
908
- productId,
909
- variantId: variant.id,
910
- quantity: rule.min
911
- });
912
- return next;
913
- });
914
- },
915
- [bundle]
916
- );
917
- const deselect = useCallback5((productId, variantId) => {
918
- setSelections((prev) => {
919
- if (!prev.has(`${productId}:${variantId}`)) return prev;
920
- const next = new Map(prev);
921
- next.delete(`${productId}:${variantId}`);
922
- return next;
923
- });
924
- }, []);
925
- const setQuantity = useCallback5(
926
- (productId, variant, quantity) => {
927
- if (!bundle) return;
928
- const rule = ruleFor(bundle, productId);
929
- const maxAddable = maxAddableQuantity(variant, rule.max, 0);
930
- const key = `${productId}:${variant.id}`;
931
- setSelections((prev) => {
932
- const next = new Map(prev);
933
- if (quantity < rule.min || maxAddable < rule.min) {
934
- next.delete(key);
935
- return next;
936
- }
937
- const clamped = Math.min(quantity, maxAddable);
938
- next.set(key, {
939
- productId,
940
- variantId: variant.id,
941
- quantity: clamped
942
- });
943
- return next;
944
- });
945
- },
946
- [bundle]
947
- );
948
- const handleAddToCart = useCallback5(async () => {
949
- if (!bundle || !valid) return;
950
- const byVariant = /* @__PURE__ */ new Map();
951
- for (const sel of selections.values()) {
952
- if (sel.quantity <= 0) continue;
953
- byVariant.set(
954
- sel.variantId,
955
- (byVariant.get(sel.variantId) ?? 0) + sel.quantity
956
- );
957
- }
958
- const lines = Array.from(byVariant.entries()).map(
959
- ([variantId, quantity]) => ({
960
- merchandiseId: variantId,
961
- quantity,
962
- attributes: [
963
- { key: "_lime_bundle_gid", value: bundle.id },
964
- { key: "_lime_bundle_type", value: bundle.bundleType }
965
- ]
966
- })
967
- );
968
- setAddingToCart(true);
969
- setCartError(null);
970
- try {
971
- await onAddToCart(lines);
972
- const totalPrice = lines.reduce((sum, line) => {
973
- const product = bundle.products.find(
974
- (p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
975
- );
976
- const variant = product?.variants.nodes.find(
977
- (v) => v.id === line.merchandiseId
978
- );
979
- return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
980
- }, 0);
981
- trackAddToCart({
982
- quantity: totalQuantity,
983
- totalPrice: Math.round(totalPrice * 100) / 100
984
- });
985
- setSelections(/* @__PURE__ */ new Map());
986
- } catch (err) {
987
- const error = err instanceof Error ? err : new Error(String(err));
988
- setCartError(error.message);
989
- onError?.(error);
990
- } finally {
991
- setAddingToCart(false);
992
- }
993
- }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
994
- if (result.status === "loading") {
995
- return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
996
- /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
997
- /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
998
- ] });
999
- }
1000
- if (result.status === "error") return null;
1001
- if (!bundle) return null;
1002
- const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
1003
- return /* @__PURE__ */ jsxs3(
1004
- "div",
1005
- {
1006
- ref: (el) => {
1007
- elementRef(el);
1008
- setConfigVarsRef(el);
1009
- },
1010
- className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
1011
- role: "region",
1012
- "aria-label": bundle.title,
1013
- children: [
1014
- /* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
1015
- bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
1016
- /* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
1017
- /* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
1018
- MixMatchProductRow,
1019
- {
1020
- bundle,
1021
- product,
1022
- currency,
1023
- showStepper,
1024
- selections,
1025
- onSelect: selectVariant,
1026
- onDeselect: deselect,
1027
- onSetQuantity: setQuantity
1028
- },
1029
- product.id
1030
- )) }),
1031
- validationMessage && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
1032
- cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1033
- /* @__PURE__ */ jsx3(
1034
- "button",
1035
- {
1036
- className: "lb-bundle__cta",
1037
- onClick: handleAddToCart,
1038
- disabled: addingToCart || !valid,
1039
- "aria-busy": addingToCart,
1040
- children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
1041
- }
1042
- )
1043
- ]
1044
- }
1045
- );
1046
- }
1047
- function MixMatchProductRow({
1048
- bundle,
1049
- product,
1050
- currency,
1051
- showStepper,
1052
- selections,
1053
- onSelect,
1054
- onDeselect,
1055
- onSetQuantity
1056
- }) {
1057
- const variants = product.variants.nodes;
1058
- const optionNames = useMemo5(() => {
1059
- const first = variants[0];
1060
- return first ? first.selectedOptions.map((o) => o.name) : [];
1061
- }, [variants]);
1062
- const showPicker = variants.length > 1 && optionNames.length > 0;
1063
- const rule = ruleFor(bundle, product.id);
1064
- const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1065
- const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? variants[0] ?? null;
1066
- if (!displayVariant) return null;
1067
- const fulfillable = isVariantFulfillable2(displayVariant, rule.min);
1068
- const maxAddable = maxAddableQuantity(displayVariant, rule.max, 0);
1069
- const key = `${product.id}:${displayVariant.id}`;
1070
- const selected = selections.get(key);
1071
- const thumbImage = displayVariant.image ?? product.featuredImage;
1072
- const unitPriceText = formatUnitPrice2(
1073
- displayVariant.unitPrice,
1074
- displayVariant.unitPriceMeasurement,
1075
- currency
1076
- );
1077
- return /* @__PURE__ */ jsxs3(
903
+ return /* @__PURE__ */ jsxs2(
1078
904
  "div",
1079
905
  {
1080
- 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",
1081
909
  children: [
1082
- thumbImage && /* @__PURE__ */ jsx3(
910
+ isOos && /* @__PURE__ */ jsx2("span", { className: "lb-bundle-product-row__oos", children: "Out of stock" }),
911
+ thumbImage && /* @__PURE__ */ jsx2(
1083
912
  "img",
1084
913
  {
1085
914
  src: thumbImage.url,
@@ -1088,27 +917,40 @@ function MixMatchProductRow({
1088
917
  loading: "lazy"
1089
918
  }
1090
919
  ),
1091
- /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
1092
- /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
1093
- /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-price", children: formatMoney2(displayVariant.price.amount, currency) }),
1094
- unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1095
- shouldShowLowStockBadge2(
1096
- displayVariant,
1097
- rule.min,
1098
- bundle.widgetConfig.lowStockThreshold,
1099
- bundle.widgetConfig.showLowStockBadge
1100
- ) && /* @__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: [
1101
943
  "Only ",
1102
944
  displayVariant.quantityAvailable,
1103
945
  " left"
1104
946
  ] }),
1105
- 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) => {
1106
948
  const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1107
949
  value: o.value,
1108
950
  label: o.value,
1109
951
  disabled: o.disabled
1110
952
  }));
1111
- return /* @__PURE__ */ jsx3(
953
+ return /* @__PURE__ */ jsx2(
1112
954
  VariantDropdown,
1113
955
  {
1114
956
  options: dropdownOptions,
@@ -1120,171 +962,2842 @@ function MixMatchProductRow({
1120
962
  );
1121
963
  }) })
1122
964
  ] }),
1123
- /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? showStepper ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
1124
- /* @__PURE__ */ jsx3(
1125
- "button",
1126
- {
1127
- "aria-label": `Decrease ${product.title}`,
1128
- onClick: () => onSetQuantity(
1129
- product.id,
1130
- displayVariant,
1131
- selected.quantity - 1
1132
- ),
1133
- disabled: selected.quantity <= rule.min,
1134
- children: "\u2212"
1135
- }
1136
- ),
1137
- /* @__PURE__ */ jsx3("span", { "aria-live": "polite", children: selected.quantity }),
1138
- /* @__PURE__ */ jsx3(
1139
- "button",
1140
- {
1141
- "aria-label": `Increase ${product.title}`,
1142
- onClick: () => onSetQuantity(
1143
- product.id,
1144
- displayVariant,
1145
- selected.quantity + 1
1146
- ),
1147
- disabled: selected.quantity >= maxAddable,
1148
- children: "+"
1149
- }
1150
- ),
1151
- /* @__PURE__ */ jsx3(
1152
- "button",
1153
- {
1154
- className: "lb-bundle__remove-btn",
1155
- "aria-label": `Remove ${product.title}`,
1156
- onClick: () => onDeselect(product.id, displayVariant.id),
1157
- children: "Remove"
1158
- }
1159
- )
1160
- ] }) : /* @__PURE__ */ jsx3(
1161
- "button",
1162
- {
1163
- className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
1164
- "aria-label": `Remove ${product.title}`,
1165
- onClick: () => onDeselect(product.id, displayVariant.id),
1166
- children: "Selected"
1167
- }
1168
- ) : /* @__PURE__ */ jsx3(
1169
- "button",
1170
- {
1171
- className: "lb-bundle__select-btn",
1172
- onClick: () => onSelect(product.id, displayVariant),
1173
- disabled: !fulfillable || maxAddable <= 0,
1174
- children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
1175
- }
1176
- ) })
965
+ displayVariant && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-qty-chip", "data-qty-inline": true, children: [
966
+ "\xD7",
967
+ displayQty
968
+ ] })
1177
969
  ]
1178
970
  }
1179
971
  );
1180
972
  }
1181
973
 
1182
- // src/components/VolumeBundle.tsx
1183
- 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";
1184
976
  import {
1185
977
  formatMoney as formatMoney3,
1186
978
  formatUnitPrice as formatUnitPrice3,
1187
- calculateTierSavings,
1188
- getActiveTier,
979
+ calculateDiscount as calculateDiscount2,
1189
980
  isVariantFulfillable as isVariantFulfillable3,
1190
- shouldShowLowStockBadge as shouldShowLowStockBadge3
981
+ DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE2
1191
982
  } from "@lime-bundles/core";
1192
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1193
- function VolumeBundle(props) {
1194
- const {
1195
- shopDomain,
1196
- storefrontAccessToken,
1197
- bundleGid,
1198
- appUrl,
1199
- analyticsEnabled,
1200
- onAddToCart,
1201
- onError,
1202
- className
1203
- } = 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;
1204
3563
  const result = useBundleData({
1205
3564
  shopDomain,
1206
3565
  storefrontAccessToken,
1207
3566
  bundleGid
1208
3567
  });
3568
+ const { outOfStockBehavior } = useShopSettings({
3569
+ shopDomain,
3570
+ storefrontAccessToken
3571
+ });
1209
3572
  const { elementRef, trackAddToCart } = useAnalytics({
1210
3573
  shopDomain,
1211
3574
  appUrl: appUrl ?? `https://${shopDomain}`,
1212
3575
  bundleGid,
1213
- bundleType: "volume",
3576
+ bundleType: "multi_step",
1214
3577
  enabled: analyticsEnabled !== false
1215
3578
  });
1216
- const [quantity, setQuantity] = useState7(1);
1217
- const [addingToCart, setAddingToCart] = useState7(false);
1218
- const [cartError, setCartError] = useState7(null);
1219
- 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;
1220
3584
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
1221
- useEffect8(() => {
3585
+ const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
3586
+ useEffect15(() => {
1222
3587
  if (result.status === "error") {
1223
3588
  onError?.(result.error);
1224
3589
  return;
1225
3590
  }
1226
- if (result.status === "success" && result.bundle.bundleType !== "volume") {
3591
+ if (result.status === "success" && result.bundle.bundleType !== "multi_step") {
1227
3592
  onError?.(
1228
3593
  new Error(
1229
- `VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
3594
+ `MultiStepBundle: expected bundleType="multi_step", got "${result.bundle.bundleType}"`
1230
3595
  )
1231
3596
  );
1232
3597
  }
1233
3598
  }, [result, onError]);
1234
- const product = bundle?.products[0];
1235
- const variants = useMemo6(() => product?.variants.nodes ?? [], [product]);
1236
- const optionNames = useMemo6(() => {
1237
- const first = variants[0];
1238
- return first ? first.selectedOptions.map((o) => o.name) : [];
1239
- }, [variants]);
1240
- const showPicker = variants.length > 1 && optionNames.length > 0;
1241
- const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1242
- const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
1243
- const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable3(v, minTierQty)) ?? variants[0];
1244
- const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1245
- const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
1246
- const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
1247
- const unitPriceText = displayVariant ? formatUnitPrice3(
1248
- displayVariant.unitPrice,
1249
- displayVariant.unitPriceMeasurement,
1250
- currency
1251
- ) : null;
1252
- const tierSavings = useMemo6(
1253
- () => bundle ? calculateTierSavings(
1254
- bundle.volumeTiers,
1255
- basePrice,
1256
- quantity,
1257
- bundle.discountConfig.discountType
1258
- ) : [],
1259
- [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]
1260
3638
  );
1261
- const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
1262
- const handleAddToCart = useCallback6(async () => {
1263
- if (!bundle || !product) return;
1264
- const variant = selectedVariant ?? product.variants.nodes.find(
1265
- (v) => isVariantFulfillable3(v, quantity)
1266
- );
1267
- if (!variant) return;
1268
- const lines = [
1269
- {
1270
- merchandiseId: variant.id,
1271
- quantity,
1272
- attributes: [
1273
- { key: "_lime_bundle_gid", value: bundle.id },
1274
- { key: "_lime_bundle_type", value: bundle.bundleType }
1275
- ]
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
+ }
1276
3668
  }
1277
- ];
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
+ }));
1278
3788
  setAddingToCart(true);
1279
3789
  setCartError(null);
1280
3790
  try {
1281
3791
  await onAddToCart(lines);
1282
- 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
+ );
1283
3796
  trackAddToCart({
1284
- productId: product.id,
1285
- quantity,
1286
- totalPrice: Math.round(unitPrice * quantity * 100) / 100
3797
+ quantity: totalQuantity,
3798
+ totalPrice: Math.round(totalPrice * 100) / 100
1287
3799
  });
3800
+ setSelections(steps.map(() => []));
1288
3801
  } catch (err) {
1289
3802
  const error = err instanceof Error ? err : new Error(String(err));
1290
3803
  setCartError(error.message);
@@ -1292,169 +3805,306 @@ function VolumeBundle(props) {
1292
3805
  } finally {
1293
3806
  setAddingToCart(false);
1294
3807
  }
1295
- }, [
1296
- bundle,
1297
- product,
1298
- quantity,
1299
- activeTier,
1300
- basePrice,
1301
- onAddToCart,
1302
- onError,
1303
- trackAddToCart,
1304
- selectedVariant,
1305
- tierSavings
1306
- ]);
3808
+ }, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity, steps]);
1307
3809
  if (result.status === "loading") {
1308
- return /* @__PURE__ */ jsxs4("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1309
- /* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--title" }),
1310
- /* @__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" })
1311
3813
  ] });
1312
3814
  }
1313
3815
  if (result.status === "error") return null;
1314
- if (!bundle) return null;
1315
- if (!product) return null;
1316
- 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(
1317
3834
  "div",
1318
3835
  {
1319
3836
  ref: (el) => {
1320
3837
  elementRef(el);
1321
3838
  setConfigVarsRef(el);
1322
3839
  },
1323
- className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
3840
+ className: `lb-bundle lb-bundle--multi-step lb-mix-match lb-multi-step ${className ?? ""}`,
1324
3841
  role: "region",
1325
3842
  "aria-label": bundle.title,
3843
+ "data-required-quantity": totalMin,
1326
3844
  children: [
1327
- /* @__PURE__ */ jsx4("h3", { className: "lb-bundle__title", children: bundle.title }),
1328
- /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
1329
- thumbImage && /* @__PURE__ */ jsx4(
1330
- "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",
1331
3852
  {
1332
- src: thumbImage.url,
1333
- alt: thumbImage.altText ?? product.title,
1334
- className: "lb-bundle__product-image",
1335
- loading: "lazy"
1336
- }
1337
- ),
1338
- /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product-info", children: [
1339
- /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
1340
- /* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
1341
- formatMoney3(basePrice, currency),
1342
- " each"
1343
- ] }),
1344
- unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1345
- showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1346
- const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1347
- value: o.value,
1348
- label: o.value,
1349
- disabled: o.disabled
1350
- }));
1351
- return /* @__PURE__ */ jsx4(
1352
- 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",
1353
3860
  {
1354
- options: dropdownOptions,
1355
- value: selectedValues[optionIndex] ?? null,
1356
- onChange: (v) => setOptionValue(optionIndex, v),
1357
- ariaLabel: optionName
3861
+ className: `lb-mix-match__progress-segment${i < completedSteps ? " lb-mix-match__progress-segment--filled" : ""}`,
3862
+ "data-progress-segment": true
1358
3863
  },
1359
- optionName
1360
- );
1361
- }) })
1362
- ] })
1363
- ] }),
1364
- /* @__PURE__ */ jsx4(
1365
- "div",
1366
- {
1367
- className: "lb-bundle__tiers",
1368
- role: "table",
1369
- "aria-label": "Volume discounts",
1370
- children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
1371
- "div",
1372
- {
1373
- className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
1374
- role: "row",
1375
- children: [
1376
- /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
1377
- ts.tier.minQuantity,
1378
- "+ items"
1379
- ] }),
1380
- /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
1381
- formatMoney3(ts.unitPrice, currency),
1382
- " each"
1383
- ] }),
1384
- /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
1385
- "Save ",
1386
- ts.savingsPercent.toFixed(0),
1387
- "%"
1388
- ] })
1389
- ]
1390
- },
1391
- ts.tier.minQuantity
1392
- ))
1393
- }
1394
- ),
1395
- /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
1396
- /* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
1397
- /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
1398
- /* @__PURE__ */ jsx4(
1399
- "button",
1400
- {
1401
- "aria-label": "Decrease quantity",
1402
- onClick: () => setQuantity((q) => Math.max(1, q - 1)),
1403
- children: "\u2212"
1404
- }
1405
- ),
1406
- /* @__PURE__ */ jsx4(
1407
- "input",
3864
+ i
3865
+ ))
3866
+ }
3867
+ ),
3868
+ /* @__PURE__ */ jsxs8("div", { className: "lb-mix-match__progress-labels", children: [
3869
+ /* @__PURE__ */ jsx8(
3870
+ "span",
1408
3871
  {
1409
- id: `lb-qty-${bundle.id}`,
1410
- type: "number",
1411
- min: 1,
1412
- value: quantity,
1413
- onChange: (e) => {
1414
- const val = parseInt(e.target.value, 10);
1415
- if (!isNaN(val) && val > 0) setQuantity(val);
1416
- },
1417
- 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`
1418
3876
  }
1419
3877
  ),
1420
- /* @__PURE__ */ jsx4(
1421
- "button",
3878
+ /* @__PURE__ */ jsx8(
3879
+ "span",
1422
3880
  {
1423
- "aria-label": "Increase quantity",
1424
- onClick: () => setQuantity((q) => q + 1),
1425
- 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"
1426
3885
  }
1427
3886
  )
1428
3887
  ] })
1429
3888
  ] }),
1430
- cartError && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1431
- bundle && displayVariant && shouldShowLowStockBadge3(
1432
- displayVariant,
1433
- minTierQty,
1434
- bundle.widgetConfig.lowStockThreshold,
1435
- bundle.widgetConfig.showLowStockBadge
1436
- ) && /* @__PURE__ */ jsxs4("span", { className: "lb-bundle-low-stock-badge", children: [
1437
- "Only ",
1438
- displayVariant.quantityAvailable,
1439
- " 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
+ ] })
1440
4018
  ] }),
1441
- /* @__PURE__ */ jsx4(
4019
+ cartError && /* @__PURE__ */ jsx8("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
4020
+ /* @__PURE__ */ jsx8(
1442
4021
  "button",
1443
4022
  {
1444
4023
  className: "lb-bundle__cta",
1445
4024
  onClick: handleAddToCart,
1446
- disabled: addingToCart,
4025
+ disabled: addingToCart || !valid,
1447
4026
  "aria-busy": addingToCart,
1448
- 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
1449
4053
  }
1450
4054
  )
1451
4055
  ]
1452
4056
  }
1453
4057
  );
1454
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
+ }
1455
4105
 
1456
4106
  // src/hooks/useBundlesForProduct.ts
1457
- import { useState as useState8, useEffect as useEffect9 } from "react";
4107
+ import { useState as useState13, useEffect as useEffect16 } from "react";
1458
4108
  import {
1459
4109
  fetchBundlesForProduct,
1460
4110
  injectCustomCss as injectCustomCss2
@@ -1465,10 +4115,10 @@ var INITIAL_STATE2 = {
1465
4115
  error: null
1466
4116
  };
1467
4117
  function useBundlesForProduct(options) {
1468
- const [state, setState] = useState8(
4118
+ const [state, setState] = useState13(
1469
4119
  INITIAL_STATE2
1470
4120
  );
1471
- useEffect9(() => {
4121
+ useEffect16(() => {
1472
4122
  const controller = new AbortController();
1473
4123
  setState(INITIAL_STATE2);
1474
4124
  fetchBundlesForProduct({
@@ -1488,13 +4138,13 @@ function useBundlesForProduct(options) {
1488
4138
  error: err instanceof Error ? err : new Error(String(err))
1489
4139
  });
1490
4140
  });
1491
- fetchShopCustomCss({
4141
+ fetchShopSettings({
1492
4142
  shopDomain: options.shopDomain,
1493
4143
  storefrontAccessToken: options.storefrontAccessToken,
1494
4144
  signal: controller.signal
1495
- }).then((css) => {
4145
+ }).then((settings) => {
1496
4146
  if (controller.signal.aborted) return;
1497
- injectCustomCss2(options.shopDomain, css);
4147
+ injectCustomCss2(options.shopDomain, settings.customCss);
1498
4148
  }).catch(() => {
1499
4149
  });
1500
4150
  return () => {
@@ -1523,20 +4173,17 @@ import {
1523
4173
  computeBundleSaleCents,
1524
4174
  calculateTierSavings as calculateTierSavings2,
1525
4175
  getActiveTier as getActiveTier2,
4176
+ productsForStep as productsForStep3,
4177
+ stepHeadroom as stepHeadroom3,
1526
4178
  validateQuantity,
1527
4179
  transformImageUrl,
1528
4180
  formatCountdown,
1529
- formatMoney as formatMoney4,
1530
- formatUnitPrice as formatUnitPrice4,
1531
- calculateDiscount as calculateDiscount2,
4181
+ formatMoney as formatMoney8,
4182
+ formatUnitPrice as formatUnitPrice8,
4183
+ calculateDiscount as calculateDiscount4,
1532
4184
  reportImpression as reportImpression2,
1533
4185
  reportAddToCart as reportAddToCart2,
1534
4186
  observeImpression as observeImpression2,
1535
- getLinkGroupAssignment,
1536
- pickVariantFromWeights,
1537
- fnv1a,
1538
- setConsent,
1539
- hasConsent,
1540
4187
  injectCustomCss as injectCustomCss3,
1541
4188
  sanitizeCustomCss,
1542
4189
  createStorefrontClient as createStorefrontClient3,
@@ -1545,21 +4192,26 @@ import {
1545
4192
  parseMetaobjectBundleStrict as parseMetaobjectBundleStrict2,
1546
4193
  BUNDLE_METAOBJECT_QUERY as BUNDLE_METAOBJECT_QUERY2,
1547
4194
  BUNDLES_FOR_PRODUCT_QUERY,
1548
- 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
1549
4198
  } from "@lime-bundles/core";
1550
4199
  export {
1551
4200
  BUNDLES_FOR_PRODUCT_QUERY,
1552
4201
  BUNDLE_METAOBJECT_QUERY2 as BUNDLE_METAOBJECT_QUERY,
4202
+ BogoBundle,
1553
4203
  BundleParseError2 as BundleParseError,
4204
+ DEFAULT_OUT_OF_STOCK_BEHAVIOR3 as DEFAULT_OUT_OF_STOCK_BEHAVIOR,
1554
4205
  FixedBundle,
1555
4206
  MixMatchBundle,
1556
- SHOP_CUSTOM_CSS_QUERY2 as SHOP_CUSTOM_CSS_QUERY,
4207
+ MultiStepBundle,
4208
+ SHOP_SETTINGS_QUERY2 as SHOP_SETTINGS_QUERY,
1557
4209
  StorefrontApiError,
1558
4210
  VariantDropdown,
1559
4211
  VolumeBundle,
1560
4212
  WIDGET_CONFIG_DEFAULTS,
1561
4213
  applyWidgetConfigVars2 as applyWidgetConfigVars,
1562
- calculateDiscount2 as calculateDiscount,
4214
+ calculateDiscount4 as calculateDiscount,
1563
4215
  calculateTierSavings2 as calculateTierSavings,
1564
4216
  computeBundleSaleCents,
1565
4217
  computeFixedPricing,
@@ -1567,31 +4219,32 @@ export {
1567
4219
  fetchBundleData,
1568
4220
  fetchBundlesForProduct2 as fetchBundlesForProduct,
1569
4221
  fetchShopCustomCss,
1570
- fnv1a,
4222
+ fetchShopSettings,
4223
+ fetchShopSettingsOrDefault,
1571
4224
  formatCents,
1572
4225
  formatCountdown,
1573
- formatMoney4 as formatMoney,
1574
- formatUnitPrice4 as formatUnitPrice,
4226
+ formatMoney8 as formatMoney,
4227
+ formatUnitPrice8 as formatUnitPrice,
1575
4228
  getActiveTier2 as getActiveTier,
1576
- getLinkGroupAssignment,
1577
- hasConsent,
1578
4229
  injectCustomCss3 as injectCustomCss,
1579
4230
  mergeWidgetConfig,
1580
4231
  observeImpression2 as observeImpression,
1581
4232
  parseCents,
1582
4233
  parseMetaobjectBundle,
1583
4234
  parseMetaobjectBundleStrict2 as parseMetaobjectBundleStrict,
4235
+ parseOutOfStockBehavior2 as parseOutOfStockBehavior,
1584
4236
  percentageDiscountUnit,
1585
- pickVariantFromWeights,
4237
+ productsForStep3 as productsForStep,
1586
4238
  reportAddToCart2 as reportAddToCart,
1587
4239
  reportImpression2 as reportImpression,
1588
4240
  sanitizeCustomCss,
1589
- setConsent,
4241
+ stepHeadroom3 as stepHeadroom,
1590
4242
  thumbnailRatioVars,
1591
4243
  transformImageUrl,
1592
4244
  useAnalytics,
1593
4245
  useBundleData,
1594
4246
  useBundlesForProduct,
4247
+ useShopSettings,
1595
4248
  useVariantSelection,
1596
4249
  useWidgetConfigVars,
1597
4250
  validateQuantity