@lime-bundles/react 2.3.1 → 2.5.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,10 +1,12 @@
1
1
  // src/components/FixedBundle.tsx
2
- import { useCallback as useCallback3, useEffect as useEffect4, useState as useState3 } from "react";
2
+ import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
3
3
  import {
4
4
  formatMoney,
5
5
  formatUnitPrice,
6
6
  calculateDiscount,
7
- resolveBundleQty
7
+ resolveBundleQty,
8
+ isVariantFulfillable,
9
+ shouldShowLowStockBadge
8
10
  } from "@lime-bundles/core";
9
11
 
10
12
  // src/hooks/useBundleData.ts
@@ -208,8 +210,340 @@ function useWidgetConfigVars(config) {
208
210
  );
209
211
  }
210
212
 
211
- // src/components/FixedBundle.tsx
213
+ // src/hooks/useVariantSelection.ts
214
+ import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3, useCallback as useCallback3 } from "react";
215
+ import { variants as variantsNs } from "@lime-bundles/core";
216
+ var { findVariantByOptions, isOptionValueAvailable, toPickerVariant } = variantsNs;
217
+ function defaultInitial(variants) {
218
+ return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
219
+ }
220
+ function useVariantSelection({
221
+ variants,
222
+ optionNames,
223
+ initialVariant,
224
+ optionValuesByPosition
225
+ }) {
226
+ const seed = useMemo2(
227
+ () => initialVariant ?? defaultInitial(variants),
228
+ [initialVariant, variants]
229
+ );
230
+ const [selectedValues, setSelectedValues] = useState3(
231
+ () => seed ? seed.selectedOptions.map((o) => o.value) : optionNames.map(() => "")
232
+ );
233
+ useEffect4(() => {
234
+ if (!seed) return;
235
+ setSelectedValues(seed.selectedOptions.map((o) => o.value));
236
+ }, [seed?.id]);
237
+ const pickerVariants = useMemo2(
238
+ () => variants.map((v) => toPickerVariant(v)),
239
+ [variants]
240
+ );
241
+ const selectedVariant = useMemo2(() => {
242
+ if (selectedValues.length !== optionNames.length) return null;
243
+ const match = findVariantByOptions(pickerVariants, selectedValues);
244
+ if (!match) return null;
245
+ return variants.find((v) => v.id === match.id) ?? null;
246
+ }, [pickerVariants, selectedValues, variants, optionNames.length]);
247
+ const setOptionValue = useCallback3(
248
+ (optionIndex, value) => {
249
+ setSelectedValues((prev) => {
250
+ const next = prev.slice();
251
+ next[optionIndex] = value;
252
+ return next;
253
+ });
254
+ },
255
+ []
256
+ );
257
+ const optionsByPosition = useMemo2(() => {
258
+ if (optionValuesByPosition) {
259
+ return optionValuesByPosition.map((values) => values.slice());
260
+ }
261
+ const out = optionNames.map(() => []);
262
+ for (const v of variants) {
263
+ v.selectedOptions.forEach((o, i) => {
264
+ if (i < out.length && !out[i].includes(o.value)) out[i].push(o.value);
265
+ });
266
+ }
267
+ return out;
268
+ }, [variants, optionNames, optionValuesByPosition]);
269
+ const optionsFor = useCallback3(
270
+ (optionIndex) => {
271
+ const values = optionsByPosition[optionIndex] ?? [];
272
+ return values.map((value) => ({
273
+ value,
274
+ disabled: !isOptionValueAvailable(
275
+ pickerVariants,
276
+ optionIndex,
277
+ value,
278
+ selectedValues
279
+ )
280
+ }));
281
+ },
282
+ [optionsByPosition, pickerVariants, selectedValues]
283
+ );
284
+ return {
285
+ selectedValues,
286
+ selectedVariant,
287
+ setOptionValue,
288
+ optionsFor
289
+ };
290
+ }
291
+
292
+ // src/components/VariantDropdown.tsx
293
+ import {
294
+ useEffect as useEffect5,
295
+ useId,
296
+ useLayoutEffect,
297
+ useMemo as useMemo3,
298
+ useRef as useRef3,
299
+ useState as useState4
300
+ } from "react";
301
+ import { dropdown } from "@lime-bundles/core";
212
302
  import { jsx, jsxs } from "react/jsx-runtime";
303
+ var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = dropdown;
304
+ var ITEM_HEIGHT_PX = 32;
305
+ var LIST_PAD_Y = 8;
306
+ var MAX_VISIBLE_ITEMS = 8;
307
+ function VariantDropdown({
308
+ options,
309
+ value,
310
+ onChange,
311
+ ariaLabel,
312
+ className
313
+ }) {
314
+ const idBase = useId();
315
+ const triggerRef = useRef3(null);
316
+ const listboxRef = useRef3(null);
317
+ const typeAheadRef = useRef3(emptyTypeAheadState());
318
+ const [isOpen, setIsOpen] = useState4(false);
319
+ const [activeIndex, setActiveIndex] = useState4(-1);
320
+ const [position, setPosition] = useState4(null);
321
+ const selectedIndex = useMemo3(
322
+ () => options.findIndex((o) => o.value === value),
323
+ [options, value]
324
+ );
325
+ const optionsState = useMemo3(
326
+ () => options.map((o) => ({ disabled: !!o.disabled, label: o.label })),
327
+ [options]
328
+ );
329
+ const selectedLabel = selectedIndex >= 0 ? options[selectedIndex].label : "";
330
+ function open() {
331
+ if (isOpen) return;
332
+ setIsOpen(true);
333
+ setActiveIndex(
334
+ selectedIndex >= 0 && !optionsState[selectedIndex]?.disabled ? selectedIndex : optionsState.findIndex((o) => !o.disabled)
335
+ );
336
+ }
337
+ function close(restoreFocus) {
338
+ setIsOpen(false);
339
+ setActiveIndex(-1);
340
+ if (restoreFocus) triggerRef.current?.focus();
341
+ }
342
+ function commit(index) {
343
+ const opt = options[index];
344
+ if (!opt || opt.disabled) return;
345
+ if (opt.value !== value) onChange(opt.value);
346
+ close(true);
347
+ }
348
+ useLayoutEffect(() => {
349
+ if (!isOpen) return;
350
+ const trigger = triggerRef.current;
351
+ if (!trigger) return;
352
+ const rect = trigger.getBoundingClientRect();
353
+ if (rect.width === 0) return;
354
+ const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
355
+ const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
356
+ setPosition(
357
+ computePosition({
358
+ trigger: {
359
+ top: rect.top,
360
+ bottom: rect.bottom,
361
+ left: rect.left,
362
+ width: rect.width
363
+ },
364
+ viewportHeight: window.innerHeight,
365
+ desiredHeight
366
+ })
367
+ );
368
+ }, [isOpen, options.length]);
369
+ useEffect5(() => {
370
+ if (!isOpen) return;
371
+ const isInsideDropdown = (event) => {
372
+ const path = event.composedPath();
373
+ const trigger = triggerRef.current;
374
+ const listbox = listboxRef.current;
375
+ return trigger != null && path.includes(trigger) || listbox != null && path.includes(listbox);
376
+ };
377
+ const onPointerdown = (event) => {
378
+ if (isInsideDropdown(event)) return;
379
+ close(false);
380
+ };
381
+ const onScroll = (event) => {
382
+ if (isInsideDropdown(event)) return;
383
+ close(false);
384
+ };
385
+ const onResize = () => close(false);
386
+ document.addEventListener("pointerdown", onPointerdown, true);
387
+ window.addEventListener("scroll", onScroll, true);
388
+ window.addEventListener("resize", onResize);
389
+ return () => {
390
+ document.removeEventListener("pointerdown", onPointerdown, true);
391
+ window.removeEventListener("scroll", onScroll, true);
392
+ window.removeEventListener("resize", onResize);
393
+ };
394
+ }, [isOpen]);
395
+ useEffect5(() => {
396
+ if (!isOpen || activeIndex < 0) return;
397
+ const listbox = listboxRef.current;
398
+ if (!listbox) return;
399
+ const li = listbox.querySelector(
400
+ `#${CSS.escape(`${idBase}-opt-${activeIndex}`)}`
401
+ );
402
+ if (!li) return;
403
+ const liTop = li.offsetTop;
404
+ const liBottom = liTop + li.offsetHeight;
405
+ const visTop = listbox.scrollTop;
406
+ const visBottom = visTop + listbox.clientHeight;
407
+ if (liTop < visTop) {
408
+ listbox.scrollTop = liTop;
409
+ } else if (liBottom > visBottom) {
410
+ listbox.scrollTop = liBottom - listbox.clientHeight;
411
+ }
412
+ }, [activeIndex, isOpen, idBase]);
413
+ function onKeyDown(event) {
414
+ const action = handleKey(
415
+ {
416
+ key: event.key,
417
+ ctrlKey: event.ctrlKey,
418
+ metaKey: event.metaKey,
419
+ altKey: event.altKey,
420
+ shiftKey: event.shiftKey
421
+ },
422
+ { isOpen, activeIndex, selectedIndex, options: optionsState }
423
+ );
424
+ if (action.preventDefault) event.preventDefault();
425
+ switch (action.type) {
426
+ case "open":
427
+ open();
428
+ if (action.activeIndex >= 0) setActiveIndex(action.activeIndex);
429
+ return;
430
+ case "close":
431
+ close(action.restoreFocus);
432
+ return;
433
+ case "move-active":
434
+ setActiveIndex(action.activeIndex);
435
+ return;
436
+ case "commit":
437
+ commit(action.index);
438
+ return;
439
+ case "type-ahead": {
440
+ const result = pushTypeAheadChar(
441
+ typeAheadRef.current,
442
+ action.char,
443
+ Date.now(),
444
+ optionsState
445
+ );
446
+ typeAheadRef.current = result.newState;
447
+ if (result.matchedIndex !== null) {
448
+ if (!isOpen) open();
449
+ setActiveIndex(result.matchedIndex);
450
+ }
451
+ return;
452
+ }
453
+ case "passthrough":
454
+ return;
455
+ }
456
+ }
457
+ function onShellBlur(event) {
458
+ const next = event.relatedTarget;
459
+ if (next && event.currentTarget.contains(next)) return;
460
+ setTimeout(() => {
461
+ if (!isOpen) return;
462
+ const active = document.activeElement;
463
+ if (!triggerRef.current?.contains(active) && !listboxRef.current?.contains(active)) {
464
+ close(false);
465
+ }
466
+ }, 0);
467
+ }
468
+ const listboxId = `${idBase}-listbox`;
469
+ const activeDescendantId = activeIndex >= 0 ? `${idBase}-opt-${activeIndex}` : void 0;
470
+ const panelStyle = position ? { maxHeight: position.maxHeight } : void 0;
471
+ return /* @__PURE__ */ jsxs(
472
+ "div",
473
+ {
474
+ className: `lb-dropdown${className ? ` ${className}` : ""}`,
475
+ "data-lb-dropdown": "",
476
+ onBlur: onShellBlur,
477
+ children: [
478
+ /* @__PURE__ */ jsxs(
479
+ "button",
480
+ {
481
+ ref: triggerRef,
482
+ type: "button",
483
+ className: "lb-dropdown-trigger",
484
+ role: "combobox",
485
+ "aria-haspopup": "listbox",
486
+ "aria-expanded": isOpen,
487
+ "aria-controls": listboxId,
488
+ "aria-label": ariaLabel,
489
+ "aria-activedescendant": activeDescendantId,
490
+ onClick: (e) => {
491
+ e.preventDefault();
492
+ if (isOpen) close(false);
493
+ else open();
494
+ },
495
+ onKeyDown,
496
+ children: [
497
+ /* @__PURE__ */ jsx("span", { className: "lb-dropdown-trigger-value", children: selectedLabel }),
498
+ /* @__PURE__ */ jsx("span", { className: "lb-dropdown-chevron", "aria-hidden": "true" })
499
+ ]
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsx(
503
+ "ul",
504
+ {
505
+ ref: listboxRef,
506
+ id: listboxId,
507
+ className: "lb-dropdown-listbox",
508
+ role: "listbox",
509
+ "aria-label": ariaLabel,
510
+ hidden: !isOpen,
511
+ "data-placement": position?.placement,
512
+ style: panelStyle,
513
+ onMouseDown: (e) => e.preventDefault(),
514
+ children: options.map((opt, i) => (
515
+ // Keyboard activation lives on the trigger combobox via
516
+ // aria-activedescendant — the WAI-ARIA combobox pattern keeps
517
+ // focus on the button and tracks the active option by id, so
518
+ // the option <li> elements deliberately have no key handlers.
519
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
520
+ /* @__PURE__ */ jsx(
521
+ "li",
522
+ {
523
+ id: `${idBase}-opt-${i}`,
524
+ className: `lb-dropdown-option${i === activeIndex ? " is-active" : ""}`,
525
+ role: "option",
526
+ "aria-selected": i === selectedIndex,
527
+ "aria-disabled": opt.disabled || void 0,
528
+ "data-value": opt.value,
529
+ onClick: () => commit(i),
530
+ onMouseMove: () => {
531
+ if (!opt.disabled && i !== activeIndex) setActiveIndex(i);
532
+ },
533
+ children: opt.label
534
+ },
535
+ opt.value
536
+ )
537
+ ))
538
+ }
539
+ )
540
+ ]
541
+ }
542
+ );
543
+ }
544
+
545
+ // src/components/FixedBundle.tsx
546
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
213
547
  function FixedBundle(props) {
214
548
  const {
215
549
  shopDomain,
@@ -233,11 +567,21 @@ function FixedBundle(props) {
233
567
  bundleType: "fixed",
234
568
  enabled: analyticsEnabled !== false
235
569
  });
236
- const [addingToCart, setAddingToCart] = useState3(false);
237
- const [cartError, setCartError] = useState3(null);
570
+ const [addingToCart, setAddingToCart] = useState5(false);
571
+ const [cartError, setCartError] = useState5(null);
572
+ const [selectedVariants, setSelectedVariants] = useState5({});
573
+ const handleVariantChange = useCallback4(
574
+ (productId, variant) => {
575
+ setSelectedVariants((prev) => {
576
+ if (prev[productId] === variant) return prev;
577
+ return { ...prev, [productId]: variant };
578
+ });
579
+ },
580
+ []
581
+ );
238
582
  const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
239
583
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
240
- useEffect4(() => {
584
+ useEffect6(() => {
241
585
  if (result.status === "error") {
242
586
  onError?.(result.error);
243
587
  return;
@@ -250,10 +594,16 @@ function FixedBundle(props) {
250
594
  );
251
595
  }
252
596
  }, [result, onError]);
253
- const handleAddToCart = useCallback3(async () => {
597
+ const handleAddToCart = useCallback4(async () => {
254
598
  if (!bundle) return;
255
- const bundleLines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
256
- const variant = p.variants.nodes.find((v) => v.availableForSale);
599
+ const bundleLines = bundle.products.filter(
600
+ (p) => p.variants.nodes.some(
601
+ (v) => isVariantFulfillable(v, resolveBundleQty(bundle, p.id, v.id))
602
+ )
603
+ ).map((p) => {
604
+ const variant = selectedVariants[p.id] ?? p.variants.nodes.find(
605
+ (v) => isVariantFulfillable(v, resolveBundleQty(bundle, p.id, v.id))
606
+ );
257
607
  return {
258
608
  productId: p.id,
259
609
  variant,
@@ -297,17 +647,17 @@ function FixedBundle(props) {
297
647
  } finally {
298
648
  setAddingToCart(false);
299
649
  }
300
- }, [bundle, onAddToCart, onError, trackAddToCart]);
650
+ }, [bundle, onAddToCart, onError, trackAddToCart, selectedVariants]);
301
651
  if (result.status === "loading") {
302
- return /* @__PURE__ */ jsxs("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
303
- /* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--title" }),
304
- /* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--products" })
652
+ return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
653
+ /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
654
+ /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
305
655
  ] });
306
656
  }
307
657
  if (result.status === "error") return null;
308
658
  if (!bundle) return null;
309
659
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
310
- return /* @__PURE__ */ jsxs(
660
+ return /* @__PURE__ */ jsxs2(
311
661
  "div",
312
662
  {
313
663
  ref: (el) => {
@@ -318,38 +668,20 @@ function FixedBundle(props) {
318
668
  role: "region",
319
669
  "aria-label": bundle.title,
320
670
  children: [
321
- /* @__PURE__ */ jsx("h3", { className: "lb-bundle__title", children: bundle.title }),
322
- bundle.discountLabel && /* @__PURE__ */ jsx("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
323
- /* @__PURE__ */ jsx("div", { className: "lb-bundle__products", children: bundle.products.map((product) => {
324
- const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
325
- const priceText = variant ? formatMoney(variant.price.amount, currency) : formatMoney(
326
- product.priceRange.minVariantPrice.amount,
327
- currency
328
- );
329
- const unitPriceText = variant ? formatUnitPrice(
330
- variant.unitPrice,
331
- variant.unitPriceMeasurement,
332
- currency
333
- ) : null;
334
- return /* @__PURE__ */ jsxs("div", { className: "lb-bundle__product", part: "product", children: [
335
- product.featuredImage && /* @__PURE__ */ jsx(
336
- "img",
337
- {
338
- src: product.featuredImage.url,
339
- alt: product.featuredImage.altText ?? product.title,
340
- className: "lb-bundle__product-image",
341
- loading: "lazy"
342
- }
343
- ),
344
- /* @__PURE__ */ jsxs("div", { className: "lb-bundle__product-info", children: [
345
- /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-title", children: product.title }),
346
- /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-price", children: priceText }),
347
- unitPriceText && /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-unit-price", children: unitPriceText })
348
- ] })
349
- ] }, product.id);
350
- }) }),
351
- cartError && /* @__PURE__ */ jsx("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
352
- /* @__PURE__ */ jsx(
671
+ /* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
672
+ bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
673
+ /* @__PURE__ */ jsx2("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ jsx2(
674
+ FixedProductRow,
675
+ {
676
+ bundle,
677
+ product,
678
+ currency,
679
+ onVariantChange: handleVariantChange
680
+ },
681
+ product.id
682
+ )) }),
683
+ cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
684
+ /* @__PURE__ */ jsx2(
353
685
  "button",
354
686
  {
355
687
  className: "lb-bundle__cta",
@@ -363,16 +695,87 @@ function FixedBundle(props) {
363
695
  }
364
696
  );
365
697
  }
698
+ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
699
+ const variants = product.variants.nodes;
700
+ const optionNames = useMemo4(() => {
701
+ const first = variants[0];
702
+ return first ? first.selectedOptions.map((o) => o.name) : [];
703
+ }, [variants]);
704
+ const showPicker = variants.length > 1 && optionNames.length > 0;
705
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
706
+ variants,
707
+ optionNames
708
+ });
709
+ useEffect6(() => {
710
+ onVariantChange(product.id, selectedVariant ?? null);
711
+ }, [selectedVariant, product.id, onVariantChange]);
712
+ const displayVariant = selectedVariant ?? variants.find(
713
+ (v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
714
+ ) ?? variants[0];
715
+ const priceText = displayVariant ? formatMoney(displayVariant.price.amount, currency) : formatMoney(product.priceRange.minVariantPrice.amount, currency);
716
+ const unitPriceText = displayVariant ? formatUnitPrice(
717
+ displayVariant.unitPrice,
718
+ displayVariant.unitPriceMeasurement,
719
+ currency
720
+ ) : null;
721
+ const thumbImage = displayVariant?.image ?? product.featuredImage;
722
+ const showLowStock = !!displayVariant && shouldShowLowStockBadge(
723
+ displayVariant,
724
+ resolveBundleQty(bundle, product.id, displayVariant.id),
725
+ bundle.widgetConfig.lowStockThreshold,
726
+ bundle.widgetConfig.showLowStockBadge
727
+ );
728
+ return /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product", part: "product", children: [
729
+ thumbImage && /* @__PURE__ */ jsx2(
730
+ "img",
731
+ {
732
+ src: thumbImage.url,
733
+ alt: thumbImage.altText ?? product.title,
734
+ className: "lb-bundle__product-image",
735
+ loading: "lazy"
736
+ }
737
+ ),
738
+ /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
739
+ /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
740
+ /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
741
+ unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
742
+ showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
743
+ "Only ",
744
+ displayVariant.quantityAvailable,
745
+ " left"
746
+ ] }),
747
+ showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
748
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
749
+ value: o.value,
750
+ label: o.value,
751
+ disabled: o.disabled
752
+ }));
753
+ return /* @__PURE__ */ jsx2(
754
+ VariantDropdown,
755
+ {
756
+ options: dropdownOptions,
757
+ value: selectedValues[optionIndex] ?? null,
758
+ onChange: (v) => setOptionValue(optionIndex, v),
759
+ ariaLabel: optionName
760
+ },
761
+ optionName
762
+ );
763
+ }) })
764
+ ] })
765
+ ] });
766
+ }
366
767
 
367
768
  // src/components/MixMatchBundle.tsx
368
- import { useCallback as useCallback4, useEffect as useEffect5, useState as useState4 } from "react";
769
+ import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
369
770
  import {
370
771
  formatMoney as formatMoney2,
371
772
  formatUnitPrice as formatUnitPrice2,
372
773
  validateQuantity,
373
- resolveBundleQty as resolveBundleQty2
774
+ resolveBundleQty as resolveBundleQty2,
775
+ isVariantFulfillable as isVariantFulfillable2,
776
+ shouldShowLowStockBadge as shouldShowLowStockBadge2
374
777
  } from "@lime-bundles/core";
375
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
778
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
376
779
  function MixMatchBundle(props) {
377
780
  const {
378
781
  shopDomain,
@@ -396,14 +799,14 @@ function MixMatchBundle(props) {
396
799
  bundleType: "mix_match",
397
800
  enabled: analyticsEnabled !== false
398
801
  });
399
- const [selections, setSelections] = useState4(
802
+ const [selections, setSelections] = useState6(
400
803
  /* @__PURE__ */ new Map()
401
804
  );
402
- const [addingToCart, setAddingToCart] = useState4(false);
403
- const [cartError, setCartError] = useState4(null);
805
+ const [addingToCart, setAddingToCart] = useState6(false);
806
+ const [cartError, setCartError] = useState6(null);
404
807
  const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
405
808
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
406
- useEffect5(() => {
809
+ useEffect7(() => {
407
810
  if (result.status === "error") {
408
811
  onError?.(result.error);
409
812
  return;
@@ -421,7 +824,7 @@ function MixMatchBundle(props) {
421
824
  0
422
825
  );
423
826
  const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
424
- const toggleProduct = useCallback4(
827
+ const toggleProduct = useCallback5(
425
828
  (productId, variant) => {
426
829
  setSelections((prev) => {
427
830
  const next = new Map(prev);
@@ -441,7 +844,7 @@ function MixMatchBundle(props) {
441
844
  },
442
845
  [bundle]
443
846
  );
444
- const updateQuantity = useCallback4(
847
+ const updateQuantity = useCallback5(
445
848
  (productId, variantId, quantity) => {
446
849
  setSelections((prev) => {
447
850
  const next = new Map(prev);
@@ -456,7 +859,7 @@ function MixMatchBundle(props) {
456
859
  },
457
860
  []
458
861
  );
459
- const handleAddToCart = useCallback4(async () => {
862
+ const handleAddToCart = useCallback5(async () => {
460
863
  if (!bundle || !validation.valid) return;
461
864
  const lines = Array.from(selections.values()).map((s) => ({
462
865
  merchandiseId: s.variantId,
@@ -501,15 +904,15 @@ function MixMatchBundle(props) {
501
904
  totalQuantity
502
905
  ]);
503
906
  if (result.status === "loading") {
504
- return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
505
- /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
506
- /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
907
+ return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
908
+ /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
909
+ /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
507
910
  ] });
508
911
  }
509
912
  if (result.status === "error") return null;
510
913
  if (!bundle) return null;
511
914
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
512
- return /* @__PURE__ */ jsxs2(
915
+ return /* @__PURE__ */ jsxs3(
513
916
  "div",
514
917
  {
515
918
  ref: (el) => {
@@ -520,83 +923,24 @@ function MixMatchBundle(props) {
520
923
  role: "region",
521
924
  "aria-label": bundle.title,
522
925
  children: [
523
- /* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
524
- bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
525
- /* @__PURE__ */ jsx2("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
526
- /* @__PURE__ */ jsx2("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => {
527
- const defaultVariant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
528
- if (!defaultVariant) return null;
529
- const key = `${product.id}:${defaultVariant.id}`;
530
- const selected = selections.get(key);
531
- return /* @__PURE__ */ jsxs2(
532
- "div",
533
- {
534
- className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
535
- children: [
536
- product.featuredImage && /* @__PURE__ */ jsx2(
537
- "img",
538
- {
539
- src: product.featuredImage.url,
540
- alt: product.featuredImage.altText ?? product.title,
541
- className: "lb-bundle__product-image",
542
- loading: "lazy"
543
- }
544
- ),
545
- /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
546
- /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
547
- /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: formatMoney2(defaultVariant.price.amount, currency) }),
548
- (() => {
549
- const unitPriceText = formatUnitPrice2(
550
- defaultVariant.unitPrice,
551
- defaultVariant.unitPriceMeasurement,
552
- currency
553
- );
554
- return unitPriceText ? /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }) : null;
555
- })()
556
- ] }),
557
- /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__quantity-control", children: [
558
- /* @__PURE__ */ jsx2(
559
- "button",
560
- {
561
- "aria-label": `Decrease ${product.title}`,
562
- onClick: () => updateQuantity(
563
- product.id,
564
- defaultVariant.id,
565
- selected.quantity - 1
566
- ),
567
- children: "\u2212"
568
- }
569
- ),
570
- /* @__PURE__ */ jsx2("span", { children: selected.quantity }),
571
- /* @__PURE__ */ jsx2(
572
- "button",
573
- {
574
- "aria-label": `Increase ${product.title}`,
575
- onClick: () => updateQuantity(
576
- product.id,
577
- defaultVariant.id,
578
- selected.quantity + 1
579
- ),
580
- children: "+"
581
- }
582
- )
583
- ] }) : /* @__PURE__ */ jsx2(
584
- "button",
585
- {
586
- className: "lb-bundle__select-btn",
587
- onClick: () => toggleProduct(product.id, defaultVariant),
588
- disabled: !defaultVariant.availableForSale,
589
- children: defaultVariant.availableForSale ? "Select" : "Sold out"
590
- }
591
- ) })
592
- ]
593
- },
594
- product.id
595
- );
596
- }) }),
597
- validation.message && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
598
- cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
599
- /* @__PURE__ */ jsx2(
926
+ /* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
927
+ bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
928
+ /* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
929
+ /* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
930
+ MixMatchProductRow,
931
+ {
932
+ bundle,
933
+ product,
934
+ currency,
935
+ selections,
936
+ onToggle: toggleProduct,
937
+ onUpdateQuantity: updateQuantity
938
+ },
939
+ product.id
940
+ )) }),
941
+ validation.message && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
942
+ cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
943
+ /* @__PURE__ */ jsx3(
600
944
  "button",
601
945
  {
602
946
  className: "lb-bundle__cta",
@@ -610,16 +954,139 @@ function MixMatchBundle(props) {
610
954
  }
611
955
  );
612
956
  }
957
+ function MixMatchProductRow({
958
+ bundle,
959
+ product,
960
+ currency,
961
+ selections,
962
+ onToggle,
963
+ onUpdateQuantity
964
+ }) {
965
+ const variants = product.variants.nodes;
966
+ const optionNames = useMemo5(() => {
967
+ const first = variants[0];
968
+ return first ? first.selectedOptions.map((o) => o.name) : [];
969
+ }, [variants]);
970
+ const showPicker = variants.length > 1 && optionNames.length > 0;
971
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
972
+ const displayVariant = selectedVariant ?? variants.find(
973
+ (v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, product.id, v.id))
974
+ ) ?? variants[0] ?? null;
975
+ if (!displayVariant) return null;
976
+ const displayVariantQty = resolveBundleQty2(
977
+ bundle,
978
+ product.id,
979
+ displayVariant.id
980
+ );
981
+ const displayVariantFulfillable = isVariantFulfillable2(
982
+ displayVariant,
983
+ displayVariantQty
984
+ );
985
+ const key = `${product.id}:${displayVariant.id}`;
986
+ const selected = selections.get(key);
987
+ const thumbImage = displayVariant.image ?? product.featuredImage;
988
+ const unitPriceText = formatUnitPrice2(
989
+ displayVariant.unitPrice,
990
+ displayVariant.unitPriceMeasurement,
991
+ currency
992
+ );
993
+ return /* @__PURE__ */ jsxs3(
994
+ "div",
995
+ {
996
+ className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
997
+ children: [
998
+ thumbImage && /* @__PURE__ */ jsx3(
999
+ "img",
1000
+ {
1001
+ src: thumbImage.url,
1002
+ alt: thumbImage.altText ?? product.title,
1003
+ className: "lb-bundle__product-image",
1004
+ loading: "lazy"
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
1008
+ /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
1009
+ /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-price", children: formatMoney2(displayVariant.price.amount, currency) }),
1010
+ unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1011
+ shouldShowLowStockBadge2(
1012
+ displayVariant,
1013
+ displayVariantQty,
1014
+ bundle.widgetConfig.lowStockThreshold,
1015
+ bundle.widgetConfig.showLowStockBadge
1016
+ ) && /* @__PURE__ */ jsxs3("span", { className: "lb-bundle-low-stock-badge", children: [
1017
+ "Only ",
1018
+ displayVariant.quantityAvailable,
1019
+ " left"
1020
+ ] }),
1021
+ showPicker && !selected && /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1022
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1023
+ value: o.value,
1024
+ label: o.value,
1025
+ disabled: o.disabled
1026
+ }));
1027
+ return /* @__PURE__ */ jsx3(
1028
+ VariantDropdown,
1029
+ {
1030
+ options: dropdownOptions,
1031
+ value: selectedValues[optionIndex] ?? null,
1032
+ onChange: (v) => setOptionValue(optionIndex, v),
1033
+ ariaLabel: optionName
1034
+ },
1035
+ optionName
1036
+ );
1037
+ }) })
1038
+ ] }),
1039
+ /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
1040
+ /* @__PURE__ */ jsx3(
1041
+ "button",
1042
+ {
1043
+ "aria-label": `Decrease ${product.title}`,
1044
+ onClick: () => onUpdateQuantity(
1045
+ product.id,
1046
+ displayVariant.id,
1047
+ selected.quantity - 1
1048
+ ),
1049
+ children: "\u2212"
1050
+ }
1051
+ ),
1052
+ /* @__PURE__ */ jsx3("span", { children: selected.quantity }),
1053
+ /* @__PURE__ */ jsx3(
1054
+ "button",
1055
+ {
1056
+ "aria-label": `Increase ${product.title}`,
1057
+ onClick: () => onUpdateQuantity(
1058
+ product.id,
1059
+ displayVariant.id,
1060
+ selected.quantity + 1
1061
+ ),
1062
+ children: "+"
1063
+ }
1064
+ )
1065
+ ] }) : /* @__PURE__ */ jsx3(
1066
+ "button",
1067
+ {
1068
+ className: "lb-bundle__select-btn",
1069
+ onClick: () => onToggle(product.id, displayVariant),
1070
+ disabled: !displayVariantFulfillable,
1071
+ children: displayVariantFulfillable ? "Select" : "Sold out"
1072
+ }
1073
+ ) })
1074
+ ]
1075
+ }
1076
+ );
1077
+ }
613
1078
 
614
1079
  // src/components/VolumeBundle.tsx
615
- import { useCallback as useCallback5, useEffect as useEffect6, useState as useState5 } from "react";
1080
+ import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo6, useState as useState7 } from "react";
616
1081
  import {
617
1082
  formatMoney as formatMoney3,
618
1083
  formatUnitPrice as formatUnitPrice3,
619
1084
  calculateTierSavings,
620
- getActiveTier
1085
+ getActiveTier,
1086
+ isVariantFulfillable as isVariantFulfillable3,
1087
+ shouldShowLowStockBadge as shouldShowLowStockBadge3
621
1088
  } from "@lime-bundles/core";
622
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1089
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
623
1090
  function VolumeBundle(props) {
624
1091
  const {
625
1092
  shopDomain,
@@ -643,12 +1110,12 @@ function VolumeBundle(props) {
643
1110
  bundleType: "volume",
644
1111
  enabled: analyticsEnabled !== false
645
1112
  });
646
- const [quantity, setQuantity] = useState5(1);
647
- const [addingToCart, setAddingToCart] = useState5(false);
648
- const [cartError, setCartError] = useState5(null);
1113
+ const [quantity, setQuantity] = useState7(1);
1114
+ const [addingToCart, setAddingToCart] = useState7(false);
1115
+ const [cartError, setCartError] = useState7(null);
649
1116
  const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
650
1117
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
651
- useEffect6(() => {
1118
+ useEffect8(() => {
652
1119
  if (result.status === "error") {
653
1120
  onError?.(result.error);
654
1121
  return;
@@ -662,18 +1129,38 @@ function VolumeBundle(props) {
662
1129
  }
663
1130
  }, [result, onError]);
664
1131
  const product = bundle?.products[0];
665
- const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1132
+ const variants = useMemo6(() => product?.variants.nodes ?? [], [product]);
1133
+ const optionNames = useMemo6(() => {
1134
+ const first = variants[0];
1135
+ return first ? first.selectedOptions.map((o) => o.name) : [];
1136
+ }, [variants]);
1137
+ const showPicker = variants.length > 1 && optionNames.length > 0;
1138
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1139
+ const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
1140
+ const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable3(v, minTierQty)) ?? variants[0];
1141
+ const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
666
1142
  const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
667
- const tierSavings = bundle ? calculateTierSavings(
668
- bundle.volumeTiers,
669
- basePrice,
670
- quantity,
671
- bundle.discountConfig.discountType
672
- ) : [];
1143
+ const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
1144
+ const unitPriceText = displayVariant ? formatUnitPrice3(
1145
+ displayVariant.unitPrice,
1146
+ displayVariant.unitPriceMeasurement,
1147
+ currency
1148
+ ) : null;
1149
+ const tierSavings = useMemo6(
1150
+ () => bundle ? calculateTierSavings(
1151
+ bundle.volumeTiers,
1152
+ basePrice,
1153
+ quantity,
1154
+ bundle.discountConfig.discountType
1155
+ ) : [],
1156
+ [bundle, basePrice, quantity]
1157
+ );
673
1158
  const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
674
- const handleAddToCart = useCallback5(async () => {
1159
+ const handleAddToCart = useCallback6(async () => {
675
1160
  if (!bundle || !product) return;
676
- const variant = product.variants.nodes.find((v) => v.availableForSale);
1161
+ const variant = selectedVariant ?? product.variants.nodes.find(
1162
+ (v) => isVariantFulfillable3(v, quantity)
1163
+ );
677
1164
  if (!variant) return;
678
1165
  const lines = [
679
1166
  {
@@ -710,18 +1197,20 @@ function VolumeBundle(props) {
710
1197
  basePrice,
711
1198
  onAddToCart,
712
1199
  onError,
713
- trackAddToCart
1200
+ trackAddToCart,
1201
+ selectedVariant,
1202
+ tierSavings
714
1203
  ]);
715
1204
  if (result.status === "loading") {
716
- return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
717
- /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
718
- /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--tiers" })
1205
+ return /* @__PURE__ */ jsxs4("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1206
+ /* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--title" }),
1207
+ /* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--tiers" })
719
1208
  ] });
720
1209
  }
721
1210
  if (result.status === "error") return null;
722
1211
  if (!bundle) return null;
723
1212
  if (!product) return null;
724
- return /* @__PURE__ */ jsxs3(
1213
+ return /* @__PURE__ */ jsxs4(
725
1214
  "div",
726
1215
  {
727
1216
  ref: (el) => {
@@ -732,55 +1221,64 @@ function VolumeBundle(props) {
732
1221
  role: "region",
733
1222
  "aria-label": bundle.title,
734
1223
  children: [
735
- /* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
736
- /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
737
- product.featuredImage && /* @__PURE__ */ jsx3(
1224
+ /* @__PURE__ */ jsx4("h3", { className: "lb-bundle__title", children: bundle.title }),
1225
+ /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
1226
+ thumbImage && /* @__PURE__ */ jsx4(
738
1227
  "img",
739
1228
  {
740
- src: product.featuredImage.url,
741
- alt: product.featuredImage.altText ?? product.title,
1229
+ src: thumbImage.url,
1230
+ alt: thumbImage.altText ?? product.title,
742
1231
  className: "lb-bundle__product-image",
743
1232
  loading: "lazy"
744
1233
  }
745
1234
  ),
746
- /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
747
- /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
748
- /* @__PURE__ */ jsxs3("p", { className: "lb-bundle__product-price", children: [
1235
+ /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product-info", children: [
1236
+ /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
1237
+ /* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
749
1238
  formatMoney3(basePrice, currency),
750
1239
  " each"
751
1240
  ] }),
752
- (() => {
753
- const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
754
- const unitPriceText = variant ? formatUnitPrice3(
755
- variant.unitPrice,
756
- variant.unitPriceMeasurement,
757
- currency
758
- ) : null;
759
- return unitPriceText ? /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }) : null;
760
- })()
1241
+ unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1242
+ showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1243
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1244
+ value: o.value,
1245
+ label: o.value,
1246
+ disabled: o.disabled
1247
+ }));
1248
+ return /* @__PURE__ */ jsx4(
1249
+ VariantDropdown,
1250
+ {
1251
+ options: dropdownOptions,
1252
+ value: selectedValues[optionIndex] ?? null,
1253
+ onChange: (v) => setOptionValue(optionIndex, v),
1254
+ ariaLabel: optionName
1255
+ },
1256
+ optionName
1257
+ );
1258
+ }) })
761
1259
  ] })
762
1260
  ] }),
763
- /* @__PURE__ */ jsx3(
1261
+ /* @__PURE__ */ jsx4(
764
1262
  "div",
765
1263
  {
766
1264
  className: "lb-bundle__tiers",
767
1265
  role: "table",
768
1266
  "aria-label": "Volume discounts",
769
- children: tierSavings.map((ts) => /* @__PURE__ */ jsxs3(
1267
+ children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
770
1268
  "div",
771
1269
  {
772
1270
  className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
773
1271
  role: "row",
774
1272
  children: [
775
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
1273
+ /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
776
1274
  ts.tier.minQuantity,
777
1275
  "+ items"
778
1276
  ] }),
779
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-price", role: "cell", children: [
1277
+ /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
780
1278
  formatMoney3(ts.unitPrice, currency),
781
1279
  " each"
782
1280
  ] }),
783
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
1281
+ /* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
784
1282
  "Save ",
785
1283
  ts.savingsPercent.toFixed(0),
786
1284
  "%"
@@ -791,10 +1289,10 @@ function VolumeBundle(props) {
791
1289
  ))
792
1290
  }
793
1291
  ),
794
- /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-selector", children: [
795
- /* @__PURE__ */ jsx3("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
796
- /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
797
- /* @__PURE__ */ jsx3(
1292
+ /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
1293
+ /* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
1294
+ /* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
1295
+ /* @__PURE__ */ jsx4(
798
1296
  "button",
799
1297
  {
800
1298
  "aria-label": "Decrease quantity",
@@ -802,7 +1300,7 @@ function VolumeBundle(props) {
802
1300
  children: "\u2212"
803
1301
  }
804
1302
  ),
805
- /* @__PURE__ */ jsx3(
1303
+ /* @__PURE__ */ jsx4(
806
1304
  "input",
807
1305
  {
808
1306
  id: `lb-qty-${bundle.id}`,
@@ -816,7 +1314,7 @@ function VolumeBundle(props) {
816
1314
  className: "lb-bundle__quantity-input"
817
1315
  }
818
1316
  ),
819
- /* @__PURE__ */ jsx3(
1317
+ /* @__PURE__ */ jsx4(
820
1318
  "button",
821
1319
  {
822
1320
  "aria-label": "Increase quantity",
@@ -826,8 +1324,18 @@ function VolumeBundle(props) {
826
1324
  )
827
1325
  ] })
828
1326
  ] }),
829
- cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
830
- /* @__PURE__ */ jsx3(
1327
+ cartError && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1328
+ bundle && displayVariant && shouldShowLowStockBadge3(
1329
+ displayVariant,
1330
+ minTierQty,
1331
+ bundle.widgetConfig.lowStockThreshold,
1332
+ bundle.widgetConfig.showLowStockBadge
1333
+ ) && /* @__PURE__ */ jsxs4("span", { className: "lb-bundle-low-stock-badge", children: [
1334
+ "Only ",
1335
+ displayVariant.quantityAvailable,
1336
+ " left"
1337
+ ] }),
1338
+ /* @__PURE__ */ jsx4(
831
1339
  "button",
832
1340
  {
833
1341
  className: "lb-bundle__cta",
@@ -843,7 +1351,7 @@ function VolumeBundle(props) {
843
1351
  }
844
1352
 
845
1353
  // src/hooks/useBundlesForProduct.ts
846
- import { useState as useState6, useEffect as useEffect7 } from "react";
1354
+ import { useState as useState8, useEffect as useEffect9 } from "react";
847
1355
  import {
848
1356
  fetchBundlesForProduct,
849
1357
  injectCustomCss as injectCustomCss2
@@ -854,10 +1362,10 @@ var INITIAL_STATE2 = {
854
1362
  error: null
855
1363
  };
856
1364
  function useBundlesForProduct(options) {
857
- const [state, setState] = useState6(
1365
+ const [state, setState] = useState8(
858
1366
  INITIAL_STATE2
859
1367
  );
860
- useEffect7(() => {
1368
+ useEffect9(() => {
861
1369
  const controller = new AbortController();
862
1370
  setState(INITIAL_STATE2);
863
1371
  fetchBundlesForProduct({
@@ -943,6 +1451,7 @@ export {
943
1451
  MixMatchBundle,
944
1452
  SHOP_CUSTOM_CSS_QUERY2 as SHOP_CUSTOM_CSS_QUERY,
945
1453
  StorefrontApiError,
1454
+ VariantDropdown,
946
1455
  VolumeBundle,
947
1456
  WIDGET_CONFIG_DEFAULTS,
948
1457
  applyABVariantB,
@@ -978,6 +1487,7 @@ export {
978
1487
  useAnalytics,
979
1488
  useBundleData,
980
1489
  useBundlesForProduct,
1490
+ useVariantSelection,
981
1491
  useWidgetConfigVars,
982
1492
  validateQuantity2 as validateQuantity
983
1493
  };