@lime-bundles/react 5.0.0 → 6.1.0

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