@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.cjs CHANGED
@@ -20,56 +20,58 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- BUNDLES_FOR_PRODUCT_QUERY: () => import_core11.BUNDLES_FOR_PRODUCT_QUERY,
24
- BUNDLE_METAOBJECT_QUERY: () => import_core11.BUNDLE_METAOBJECT_QUERY,
25
- BundleParseError: () => import_core10.BundleParseError,
23
+ BUNDLES_FOR_PRODUCT_QUERY: () => import_core13.BUNDLES_FOR_PRODUCT_QUERY,
24
+ BUNDLE_METAOBJECT_QUERY: () => import_core13.BUNDLE_METAOBJECT_QUERY,
25
+ BundleParseError: () => import_core12.BundleParseError,
26
26
  FixedBundle: () => FixedBundle,
27
27
  MixMatchBundle: () => MixMatchBundle,
28
- SHOP_CUSTOM_CSS_QUERY: () => import_core11.SHOP_CUSTOM_CSS_QUERY,
29
- StorefrontApiError: () => import_core10.StorefrontApiError,
28
+ SHOP_CUSTOM_CSS_QUERY: () => import_core13.SHOP_CUSTOM_CSS_QUERY,
29
+ StorefrontApiError: () => import_core12.StorefrontApiError,
30
+ VariantDropdown: () => VariantDropdown,
30
31
  VolumeBundle: () => VolumeBundle,
31
- WIDGET_CONFIG_DEFAULTS: () => import_core11.WIDGET_CONFIG_DEFAULTS,
32
- applyABVariantB: () => import_core11.applyABVariantB,
33
- applyWidgetConfigVars: () => import_core11.applyWidgetConfigVars,
34
- calculateDiscount: () => import_core11.calculateDiscount,
35
- calculateTierSavings: () => import_core11.calculateTierSavings,
36
- computeBundleSaleCents: () => import_core11.computeBundleSaleCents,
37
- computeFixedPricing: () => import_core11.computeFixedPricing,
38
- createStorefrontClient: () => import_core11.createStorefrontClient,
32
+ WIDGET_CONFIG_DEFAULTS: () => import_core13.WIDGET_CONFIG_DEFAULTS,
33
+ applyABVariantB: () => import_core13.applyABVariantB,
34
+ applyWidgetConfigVars: () => import_core13.applyWidgetConfigVars,
35
+ calculateDiscount: () => import_core13.calculateDiscount,
36
+ calculateTierSavings: () => import_core13.calculateTierSavings,
37
+ computeBundleSaleCents: () => import_core13.computeBundleSaleCents,
38
+ computeFixedPricing: () => import_core13.computeFixedPricing,
39
+ createStorefrontClient: () => import_core13.createStorefrontClient,
39
40
  fetchBundleData: () => fetchBundleData,
40
- fetchBundlesForProduct: () => import_core11.fetchBundlesForProduct,
41
+ fetchBundlesForProduct: () => import_core13.fetchBundlesForProduct,
41
42
  fetchShopCustomCss: () => fetchShopCustomCss,
42
- formatCents: () => import_core11.formatCents,
43
- formatCountdown: () => import_core11.formatCountdown,
44
- formatMoney: () => import_core11.formatMoney,
45
- formatUnitPrice: () => import_core11.formatUnitPrice,
46
- getABTestAssignment: () => import_core11.getABTestAssignment,
47
- getActiveTier: () => import_core11.getActiveTier,
48
- hasConsent: () => import_core11.hasConsent,
49
- injectCustomCss: () => import_core11.injectCustomCss,
50
- mergeWidgetConfig: () => import_core11.mergeWidgetConfig,
51
- observeImpression: () => import_core11.observeImpression,
52
- parseCents: () => import_core11.parseCents,
53
- parseMetaobjectBundle: () => import_core11.parseMetaobjectBundle,
54
- parseMetaobjectBundleStrict: () => import_core11.parseMetaobjectBundleStrict,
55
- percentageDiscountUnit: () => import_core11.percentageDiscountUnit,
56
- reportAddToCart: () => import_core11.reportAddToCart,
57
- reportImpression: () => import_core11.reportImpression,
58
- sanitizeCustomCss: () => import_core11.sanitizeCustomCss,
59
- setConsent: () => import_core11.setConsent,
60
- thumbnailRatioVars: () => import_core11.thumbnailRatioVars,
61
- transformImageUrl: () => import_core11.transformImageUrl,
43
+ formatCents: () => import_core13.formatCents,
44
+ formatCountdown: () => import_core13.formatCountdown,
45
+ formatMoney: () => import_core13.formatMoney,
46
+ formatUnitPrice: () => import_core13.formatUnitPrice,
47
+ getABTestAssignment: () => import_core13.getABTestAssignment,
48
+ getActiveTier: () => import_core13.getActiveTier,
49
+ hasConsent: () => import_core13.hasConsent,
50
+ injectCustomCss: () => import_core13.injectCustomCss,
51
+ mergeWidgetConfig: () => import_core13.mergeWidgetConfig,
52
+ observeImpression: () => import_core13.observeImpression,
53
+ parseCents: () => import_core13.parseCents,
54
+ parseMetaobjectBundle: () => import_core13.parseMetaobjectBundle,
55
+ parseMetaobjectBundleStrict: () => import_core13.parseMetaobjectBundleStrict,
56
+ percentageDiscountUnit: () => import_core13.percentageDiscountUnit,
57
+ reportAddToCart: () => import_core13.reportAddToCart,
58
+ reportImpression: () => import_core13.reportImpression,
59
+ sanitizeCustomCss: () => import_core13.sanitizeCustomCss,
60
+ setConsent: () => import_core13.setConsent,
61
+ thumbnailRatioVars: () => import_core13.thumbnailRatioVars,
62
+ transformImageUrl: () => import_core13.transformImageUrl,
62
63
  useAnalytics: () => useAnalytics,
63
64
  useBundleData: () => useBundleData,
64
65
  useBundlesForProduct: () => useBundlesForProduct,
66
+ useVariantSelection: () => useVariantSelection,
65
67
  useWidgetConfigVars: () => useWidgetConfigVars,
66
- validateQuantity: () => import_core11.validateQuantity
68
+ validateQuantity: () => import_core13.validateQuantity
67
69
  });
68
70
  module.exports = __toCommonJS(index_exports);
69
71
 
70
72
  // src/components/FixedBundle.tsx
71
- var import_react4 = require("react");
72
- var import_core6 = require("@lime-bundles/core");
73
+ var import_react6 = require("react");
74
+ var import_core8 = require("@lime-bundles/core");
73
75
 
74
76
  // src/hooks/useBundleData.ts
75
77
  var import_react = require("react");
@@ -260,8 +262,333 @@ function useWidgetConfigVars(config) {
260
262
  );
261
263
  }
262
264
 
263
- // src/components/FixedBundle.tsx
265
+ // src/hooks/useVariantSelection.ts
266
+ var import_react4 = require("react");
267
+ var import_core6 = require("@lime-bundles/core");
268
+ var { findVariantByOptions, isOptionValueAvailable, toPickerVariant } = import_core6.variants;
269
+ function defaultInitial(variants) {
270
+ return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
271
+ }
272
+ function useVariantSelection({
273
+ variants,
274
+ optionNames,
275
+ initialVariant,
276
+ optionValuesByPosition
277
+ }) {
278
+ const seed = (0, import_react4.useMemo)(
279
+ () => initialVariant ?? defaultInitial(variants),
280
+ [initialVariant, variants]
281
+ );
282
+ const [selectedValues, setSelectedValues] = (0, import_react4.useState)(
283
+ () => seed ? seed.selectedOptions.map((o) => o.value) : optionNames.map(() => "")
284
+ );
285
+ (0, import_react4.useEffect)(() => {
286
+ if (!seed) return;
287
+ setSelectedValues(seed.selectedOptions.map((o) => o.value));
288
+ }, [seed?.id]);
289
+ const pickerVariants = (0, import_react4.useMemo)(
290
+ () => variants.map((v) => toPickerVariant(v)),
291
+ [variants]
292
+ );
293
+ const selectedVariant = (0, import_react4.useMemo)(() => {
294
+ if (selectedValues.length !== optionNames.length) return null;
295
+ const match = findVariantByOptions(pickerVariants, selectedValues);
296
+ if (!match) return null;
297
+ return variants.find((v) => v.id === match.id) ?? null;
298
+ }, [pickerVariants, selectedValues, variants, optionNames.length]);
299
+ const setOptionValue = (0, import_react4.useCallback)(
300
+ (optionIndex, value) => {
301
+ setSelectedValues((prev) => {
302
+ const next = prev.slice();
303
+ next[optionIndex] = value;
304
+ return next;
305
+ });
306
+ },
307
+ []
308
+ );
309
+ const optionsByPosition = (0, import_react4.useMemo)(() => {
310
+ if (optionValuesByPosition) {
311
+ return optionValuesByPosition.map((values) => values.slice());
312
+ }
313
+ const out = optionNames.map(() => []);
314
+ for (const v of variants) {
315
+ v.selectedOptions.forEach((o, i) => {
316
+ if (i < out.length && !out[i].includes(o.value)) out[i].push(o.value);
317
+ });
318
+ }
319
+ return out;
320
+ }, [variants, optionNames, optionValuesByPosition]);
321
+ const optionsFor = (0, import_react4.useCallback)(
322
+ (optionIndex) => {
323
+ const values = optionsByPosition[optionIndex] ?? [];
324
+ return values.map((value) => ({
325
+ value,
326
+ disabled: !isOptionValueAvailable(
327
+ pickerVariants,
328
+ optionIndex,
329
+ value,
330
+ selectedValues
331
+ )
332
+ }));
333
+ },
334
+ [optionsByPosition, pickerVariants, selectedValues]
335
+ );
336
+ return {
337
+ selectedValues,
338
+ selectedVariant,
339
+ setOptionValue,
340
+ optionsFor
341
+ };
342
+ }
343
+
344
+ // src/components/VariantDropdown.tsx
345
+ var import_react5 = require("react");
346
+ var import_core7 = require("@lime-bundles/core");
264
347
  var import_jsx_runtime = require("react/jsx-runtime");
348
+ var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = import_core7.dropdown;
349
+ var ITEM_HEIGHT_PX = 32;
350
+ var LIST_PAD_Y = 8;
351
+ var MAX_VISIBLE_ITEMS = 8;
352
+ function VariantDropdown({
353
+ options,
354
+ value,
355
+ onChange,
356
+ ariaLabel,
357
+ className
358
+ }) {
359
+ const idBase = (0, import_react5.useId)();
360
+ const triggerRef = (0, import_react5.useRef)(null);
361
+ const listboxRef = (0, import_react5.useRef)(null);
362
+ const typeAheadRef = (0, import_react5.useRef)(emptyTypeAheadState());
363
+ const [isOpen, setIsOpen] = (0, import_react5.useState)(false);
364
+ const [activeIndex, setActiveIndex] = (0, import_react5.useState)(-1);
365
+ const [position, setPosition] = (0, import_react5.useState)(null);
366
+ const selectedIndex = (0, import_react5.useMemo)(
367
+ () => options.findIndex((o) => o.value === value),
368
+ [options, value]
369
+ );
370
+ const optionsState = (0, import_react5.useMemo)(
371
+ () => options.map((o) => ({ disabled: !!o.disabled, label: o.label })),
372
+ [options]
373
+ );
374
+ const selectedLabel = selectedIndex >= 0 ? options[selectedIndex].label : "";
375
+ function open() {
376
+ if (isOpen) return;
377
+ setIsOpen(true);
378
+ setActiveIndex(
379
+ selectedIndex >= 0 && !optionsState[selectedIndex]?.disabled ? selectedIndex : optionsState.findIndex((o) => !o.disabled)
380
+ );
381
+ }
382
+ function close(restoreFocus) {
383
+ setIsOpen(false);
384
+ setActiveIndex(-1);
385
+ if (restoreFocus) triggerRef.current?.focus();
386
+ }
387
+ function commit(index) {
388
+ const opt = options[index];
389
+ if (!opt || opt.disabled) return;
390
+ if (opt.value !== value) onChange(opt.value);
391
+ close(true);
392
+ }
393
+ (0, import_react5.useLayoutEffect)(() => {
394
+ if (!isOpen) return;
395
+ const trigger = triggerRef.current;
396
+ if (!trigger) return;
397
+ const rect = trigger.getBoundingClientRect();
398
+ if (rect.width === 0) return;
399
+ const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
400
+ const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
401
+ setPosition(
402
+ computePosition({
403
+ trigger: {
404
+ top: rect.top,
405
+ bottom: rect.bottom,
406
+ left: rect.left,
407
+ width: rect.width
408
+ },
409
+ viewportHeight: window.innerHeight,
410
+ desiredHeight
411
+ })
412
+ );
413
+ }, [isOpen, options.length]);
414
+ (0, import_react5.useEffect)(() => {
415
+ if (!isOpen) return;
416
+ const isInsideDropdown = (event) => {
417
+ const path = event.composedPath();
418
+ const trigger = triggerRef.current;
419
+ const listbox = listboxRef.current;
420
+ return trigger != null && path.includes(trigger) || listbox != null && path.includes(listbox);
421
+ };
422
+ const onPointerdown = (event) => {
423
+ if (isInsideDropdown(event)) return;
424
+ close(false);
425
+ };
426
+ const onScroll = (event) => {
427
+ if (isInsideDropdown(event)) return;
428
+ close(false);
429
+ };
430
+ const onResize = () => close(false);
431
+ document.addEventListener("pointerdown", onPointerdown, true);
432
+ window.addEventListener("scroll", onScroll, true);
433
+ window.addEventListener("resize", onResize);
434
+ return () => {
435
+ document.removeEventListener("pointerdown", onPointerdown, true);
436
+ window.removeEventListener("scroll", onScroll, true);
437
+ window.removeEventListener("resize", onResize);
438
+ };
439
+ }, [isOpen]);
440
+ (0, import_react5.useEffect)(() => {
441
+ if (!isOpen || activeIndex < 0) return;
442
+ const listbox = listboxRef.current;
443
+ if (!listbox) return;
444
+ const li = listbox.querySelector(
445
+ `#${CSS.escape(`${idBase}-opt-${activeIndex}`)}`
446
+ );
447
+ if (!li) return;
448
+ const liTop = li.offsetTop;
449
+ const liBottom = liTop + li.offsetHeight;
450
+ const visTop = listbox.scrollTop;
451
+ const visBottom = visTop + listbox.clientHeight;
452
+ if (liTop < visTop) {
453
+ listbox.scrollTop = liTop;
454
+ } else if (liBottom > visBottom) {
455
+ listbox.scrollTop = liBottom - listbox.clientHeight;
456
+ }
457
+ }, [activeIndex, isOpen, idBase]);
458
+ function onKeyDown(event) {
459
+ const action = handleKey(
460
+ {
461
+ key: event.key,
462
+ ctrlKey: event.ctrlKey,
463
+ metaKey: event.metaKey,
464
+ altKey: event.altKey,
465
+ shiftKey: event.shiftKey
466
+ },
467
+ { isOpen, activeIndex, selectedIndex, options: optionsState }
468
+ );
469
+ if (action.preventDefault) event.preventDefault();
470
+ switch (action.type) {
471
+ case "open":
472
+ open();
473
+ if (action.activeIndex >= 0) setActiveIndex(action.activeIndex);
474
+ return;
475
+ case "close":
476
+ close(action.restoreFocus);
477
+ return;
478
+ case "move-active":
479
+ setActiveIndex(action.activeIndex);
480
+ return;
481
+ case "commit":
482
+ commit(action.index);
483
+ return;
484
+ case "type-ahead": {
485
+ const result = pushTypeAheadChar(
486
+ typeAheadRef.current,
487
+ action.char,
488
+ Date.now(),
489
+ optionsState
490
+ );
491
+ typeAheadRef.current = result.newState;
492
+ if (result.matchedIndex !== null) {
493
+ if (!isOpen) open();
494
+ setActiveIndex(result.matchedIndex);
495
+ }
496
+ return;
497
+ }
498
+ case "passthrough":
499
+ return;
500
+ }
501
+ }
502
+ function onShellBlur(event) {
503
+ const next = event.relatedTarget;
504
+ if (next && event.currentTarget.contains(next)) return;
505
+ setTimeout(() => {
506
+ if (!isOpen) return;
507
+ const active = document.activeElement;
508
+ if (!triggerRef.current?.contains(active) && !listboxRef.current?.contains(active)) {
509
+ close(false);
510
+ }
511
+ }, 0);
512
+ }
513
+ const listboxId = `${idBase}-listbox`;
514
+ const activeDescendantId = activeIndex >= 0 ? `${idBase}-opt-${activeIndex}` : void 0;
515
+ const panelStyle = position ? { maxHeight: position.maxHeight } : void 0;
516
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
517
+ "div",
518
+ {
519
+ className: `lb-dropdown${className ? ` ${className}` : ""}`,
520
+ "data-lb-dropdown": "",
521
+ onBlur: onShellBlur,
522
+ children: [
523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
524
+ "button",
525
+ {
526
+ ref: triggerRef,
527
+ type: "button",
528
+ className: "lb-dropdown-trigger",
529
+ role: "combobox",
530
+ "aria-haspopup": "listbox",
531
+ "aria-expanded": isOpen,
532
+ "aria-controls": listboxId,
533
+ "aria-label": ariaLabel,
534
+ "aria-activedescendant": activeDescendantId,
535
+ onClick: (e) => {
536
+ e.preventDefault();
537
+ if (isOpen) close(false);
538
+ else open();
539
+ },
540
+ onKeyDown,
541
+ children: [
542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "lb-dropdown-trigger-value", children: selectedLabel }),
543
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "lb-dropdown-chevron", "aria-hidden": "true" })
544
+ ]
545
+ }
546
+ ),
547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
548
+ "ul",
549
+ {
550
+ ref: listboxRef,
551
+ id: listboxId,
552
+ className: "lb-dropdown-listbox",
553
+ role: "listbox",
554
+ "aria-label": ariaLabel,
555
+ hidden: !isOpen,
556
+ "data-placement": position?.placement,
557
+ style: panelStyle,
558
+ onMouseDown: (e) => e.preventDefault(),
559
+ children: options.map((opt, i) => (
560
+ // Keyboard activation lives on the trigger combobox via
561
+ // aria-activedescendant — the WAI-ARIA combobox pattern keeps
562
+ // focus on the button and tracks the active option by id, so
563
+ // the option <li> elements deliberately have no key handlers.
564
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
565
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
566
+ "li",
567
+ {
568
+ id: `${idBase}-opt-${i}`,
569
+ className: `lb-dropdown-option${i === activeIndex ? " is-active" : ""}`,
570
+ role: "option",
571
+ "aria-selected": i === selectedIndex,
572
+ "aria-disabled": opt.disabled || void 0,
573
+ "data-value": opt.value,
574
+ onClick: () => commit(i),
575
+ onMouseMove: () => {
576
+ if (!opt.disabled && i !== activeIndex) setActiveIndex(i);
577
+ },
578
+ children: opt.label
579
+ },
580
+ opt.value
581
+ )
582
+ ))
583
+ }
584
+ )
585
+ ]
586
+ }
587
+ );
588
+ }
589
+
590
+ // src/components/FixedBundle.tsx
591
+ var import_jsx_runtime2 = require("react/jsx-runtime");
265
592
  function FixedBundle(props) {
266
593
  const {
267
594
  shopDomain,
@@ -285,11 +612,21 @@ function FixedBundle(props) {
285
612
  bundleType: "fixed",
286
613
  enabled: analyticsEnabled !== false
287
614
  });
288
- const [addingToCart, setAddingToCart] = (0, import_react4.useState)(false);
289
- const [cartError, setCartError] = (0, import_react4.useState)(null);
615
+ const [addingToCart, setAddingToCart] = (0, import_react6.useState)(false);
616
+ const [cartError, setCartError] = (0, import_react6.useState)(null);
617
+ const [selectedVariants, setSelectedVariants] = (0, import_react6.useState)({});
618
+ const handleVariantChange = (0, import_react6.useCallback)(
619
+ (productId, variant) => {
620
+ setSelectedVariants((prev) => {
621
+ if (prev[productId] === variant) return prev;
622
+ return { ...prev, [productId]: variant };
623
+ });
624
+ },
625
+ []
626
+ );
290
627
  const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
291
628
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
292
- (0, import_react4.useEffect)(() => {
629
+ (0, import_react6.useEffect)(() => {
293
630
  if (result.status === "error") {
294
631
  onError?.(result.error);
295
632
  return;
@@ -302,14 +639,20 @@ function FixedBundle(props) {
302
639
  );
303
640
  }
304
641
  }, [result, onError]);
305
- const handleAddToCart = (0, import_react4.useCallback)(async () => {
642
+ const handleAddToCart = (0, import_react6.useCallback)(async () => {
306
643
  if (!bundle) return;
307
- const bundleLines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
308
- const variant = p.variants.nodes.find((v) => v.availableForSale);
644
+ const bundleLines = bundle.products.filter(
645
+ (p) => p.variants.nodes.some(
646
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, p.id, v.id))
647
+ )
648
+ ).map((p) => {
649
+ const variant = selectedVariants[p.id] ?? p.variants.nodes.find(
650
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, p.id, v.id))
651
+ );
309
652
  return {
310
653
  productId: p.id,
311
654
  variant,
312
- quantity: (0, import_core6.resolveBundleQty)(bundle, p.id, variant.id)
655
+ quantity: (0, import_core8.resolveBundleQty)(bundle, p.id, variant.id)
313
656
  };
314
657
  }).filter((line) => line.quantity > 0);
315
658
  if (bundleLines.length === 0) return;
@@ -327,7 +670,7 @@ function FixedBundle(props) {
327
670
  await onAddToCart(lines);
328
671
  const totalPrice = bundleLines.reduce((sum, line) => {
329
672
  const price = parseFloat(line.variant.price.amount);
330
- const discounted = (0, import_core6.calculateDiscount)(
673
+ const discounted = (0, import_core8.calculateDiscount)(
331
674
  price,
332
675
  bundle.discountConfig.discountType,
333
676
  bundle.discountConfig.discountValue
@@ -349,17 +692,17 @@ function FixedBundle(props) {
349
692
  } finally {
350
693
  setAddingToCart(false);
351
694
  }
352
- }, [bundle, onAddToCart, onError, trackAddToCart]);
695
+ }, [bundle, onAddToCart, onError, trackAddToCart, selectedVariants]);
353
696
  if (result.status === "loading") {
354
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
355
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
356
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
697
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
699
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
357
700
  ] });
358
701
  }
359
702
  if (result.status === "error") return null;
360
703
  if (!bundle) return null;
361
704
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
362
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
705
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
363
706
  "div",
364
707
  {
365
708
  ref: (el) => {
@@ -370,38 +713,20 @@ function FixedBundle(props) {
370
713
  role: "region",
371
714
  "aria-label": bundle.title,
372
715
  children: [
373
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
374
- bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
375
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => {
376
- const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
377
- const priceText = variant ? (0, import_core6.formatMoney)(variant.price.amount, currency) : (0, import_core6.formatMoney)(
378
- product.priceRange.minVariantPrice.amount,
379
- currency
380
- );
381
- const unitPriceText = variant ? (0, import_core6.formatUnitPrice)(
382
- variant.unitPrice,
383
- variant.unitPriceMeasurement,
384
- currency
385
- ) : null;
386
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
387
- product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
388
- "img",
389
- {
390
- src: product.featuredImage.url,
391
- alt: product.featuredImage.altText ?? product.title,
392
- className: "lb-bundle__product-image",
393
- loading: "lazy"
394
- }
395
- ),
396
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lb-bundle__product-info", children: [
397
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
398
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-price", children: priceText }),
399
- unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText })
400
- ] })
401
- ] }, product.id);
402
- }) }),
403
- cartError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
404
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
716
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
717
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
719
+ FixedProductRow,
720
+ {
721
+ bundle,
722
+ product,
723
+ currency,
724
+ onVariantChange: handleVariantChange
725
+ },
726
+ product.id
727
+ )) }),
728
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
729
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
405
730
  "button",
406
731
  {
407
732
  className: "lb-bundle__cta",
@@ -415,11 +740,80 @@ function FixedBundle(props) {
415
740
  }
416
741
  );
417
742
  }
743
+ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
744
+ const variants = product.variants.nodes;
745
+ const optionNames = (0, import_react6.useMemo)(() => {
746
+ const first = variants[0];
747
+ return first ? first.selectedOptions.map((o) => o.name) : [];
748
+ }, [variants]);
749
+ const showPicker = variants.length > 1 && optionNames.length > 0;
750
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
751
+ variants,
752
+ optionNames
753
+ });
754
+ (0, import_react6.useEffect)(() => {
755
+ onVariantChange(product.id, selectedVariant ?? null);
756
+ }, [selectedVariant, product.id, onVariantChange]);
757
+ const displayVariant = selectedVariant ?? variants.find(
758
+ (v) => (0, import_core8.isVariantFulfillable)(v, (0, import_core8.resolveBundleQty)(bundle, product.id, v.id))
759
+ ) ?? variants[0];
760
+ const priceText = displayVariant ? (0, import_core8.formatMoney)(displayVariant.price.amount, currency) : (0, import_core8.formatMoney)(product.priceRange.minVariantPrice.amount, currency);
761
+ const unitPriceText = displayVariant ? (0, import_core8.formatUnitPrice)(
762
+ displayVariant.unitPrice,
763
+ displayVariant.unitPriceMeasurement,
764
+ currency
765
+ ) : null;
766
+ const thumbImage = displayVariant?.image ?? product.featuredImage;
767
+ const showLowStock = !!displayVariant && (0, import_core8.shouldShowLowStockBadge)(
768
+ displayVariant,
769
+ (0, import_core8.resolveBundleQty)(bundle, product.id, displayVariant.id),
770
+ bundle.widgetConfig.lowStockThreshold,
771
+ bundle.widgetConfig.showLowStockBadge
772
+ );
773
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
774
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
775
+ "img",
776
+ {
777
+ src: thumbImage.url,
778
+ alt: thumbImage.altText ?? product.title,
779
+ className: "lb-bundle__product-image",
780
+ loading: "lazy"
781
+ }
782
+ ),
783
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
784
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: priceText }),
786
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
787
+ showLowStock && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
788
+ "Only ",
789
+ displayVariant.quantityAvailable,
790
+ " left"
791
+ ] }),
792
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
793
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
794
+ value: o.value,
795
+ label: o.value,
796
+ disabled: o.disabled
797
+ }));
798
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
799
+ VariantDropdown,
800
+ {
801
+ options: dropdownOptions,
802
+ value: selectedValues[optionIndex] ?? null,
803
+ onChange: (v) => setOptionValue(optionIndex, v),
804
+ ariaLabel: optionName
805
+ },
806
+ optionName
807
+ );
808
+ }) })
809
+ ] })
810
+ ] });
811
+ }
418
812
 
419
813
  // src/components/MixMatchBundle.tsx
420
- var import_react5 = require("react");
421
- var import_core7 = require("@lime-bundles/core");
422
- var import_jsx_runtime2 = require("react/jsx-runtime");
814
+ var import_react7 = require("react");
815
+ var import_core9 = require("@lime-bundles/core");
816
+ var import_jsx_runtime3 = require("react/jsx-runtime");
423
817
  function MixMatchBundle(props) {
424
818
  const {
425
819
  shopDomain,
@@ -443,14 +837,14 @@ function MixMatchBundle(props) {
443
837
  bundleType: "mix_match",
444
838
  enabled: analyticsEnabled !== false
445
839
  });
446
- const [selections, setSelections] = (0, import_react5.useState)(
840
+ const [selections, setSelections] = (0, import_react7.useState)(
447
841
  /* @__PURE__ */ new Map()
448
842
  );
449
- const [addingToCart, setAddingToCart] = (0, import_react5.useState)(false);
450
- const [cartError, setCartError] = (0, import_react5.useState)(null);
843
+ const [addingToCart, setAddingToCart] = (0, import_react7.useState)(false);
844
+ const [cartError, setCartError] = (0, import_react7.useState)(null);
451
845
  const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
452
846
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
453
- (0, import_react5.useEffect)(() => {
847
+ (0, import_react7.useEffect)(() => {
454
848
  if (result.status === "error") {
455
849
  onError?.(result.error);
456
850
  return;
@@ -467,8 +861,8 @@ function MixMatchBundle(props) {
467
861
  (sum, s) => sum + s.quantity,
468
862
  0
469
863
  );
470
- const validation = bundle ? (0, import_core7.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
471
- const toggleProduct = (0, import_react5.useCallback)(
864
+ const validation = bundle ? (0, import_core9.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
865
+ const toggleProduct = (0, import_react7.useCallback)(
472
866
  (productId, variant) => {
473
867
  setSelections((prev) => {
474
868
  const next = new Map(prev);
@@ -476,7 +870,7 @@ function MixMatchBundle(props) {
476
870
  if (next.has(key)) {
477
871
  next.delete(key);
478
872
  } else {
479
- const configuredQty = bundle ? (0, import_core7.resolveBundleQty)(bundle, productId, variant.id) : 1;
873
+ const configuredQty = bundle ? (0, import_core9.resolveBundleQty)(bundle, productId, variant.id) : 1;
480
874
  next.set(key, {
481
875
  productId,
482
876
  variantId: variant.id,
@@ -488,7 +882,7 @@ function MixMatchBundle(props) {
488
882
  },
489
883
  [bundle]
490
884
  );
491
- const updateQuantity = (0, import_react5.useCallback)(
885
+ const updateQuantity = (0, import_react7.useCallback)(
492
886
  (productId, variantId, quantity) => {
493
887
  setSelections((prev) => {
494
888
  const next = new Map(prev);
@@ -503,7 +897,7 @@ function MixMatchBundle(props) {
503
897
  },
504
898
  []
505
899
  );
506
- const handleAddToCart = (0, import_react5.useCallback)(async () => {
900
+ const handleAddToCart = (0, import_react7.useCallback)(async () => {
507
901
  if (!bundle || !validation.valid) return;
508
902
  const lines = Array.from(selections.values()).map((s) => ({
509
903
  merchandiseId: s.variantId,
@@ -548,15 +942,15 @@ function MixMatchBundle(props) {
548
942
  totalQuantity
549
943
  ]);
550
944
  if (result.status === "loading") {
551
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
552
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
553
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
945
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
946
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
947
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
554
948
  ] });
555
949
  }
556
950
  if (result.status === "error") return null;
557
951
  if (!bundle) return null;
558
952
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
559
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
953
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
560
954
  "div",
561
955
  {
562
956
  ref: (el) => {
@@ -567,83 +961,24 @@ function MixMatchBundle(props) {
567
961
  role: "region",
568
962
  "aria-label": bundle.title,
569
963
  children: [
570
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
571
- bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
572
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }),
573
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => {
574
- const defaultVariant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
575
- if (!defaultVariant) return null;
576
- const key = `${product.id}:${defaultVariant.id}`;
577
- const selected = selections.get(key);
578
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
579
- "div",
580
- {
581
- className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
582
- children: [
583
- product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
584
- "img",
585
- {
586
- src: product.featuredImage.url,
587
- alt: product.featuredImage.altText ?? product.title,
588
- className: "lb-bundle__product-image",
589
- loading: "lazy"
590
- }
591
- ),
592
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
593
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
594
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core7.formatMoney)(defaultVariant.price.amount, currency) }),
595
- (() => {
596
- const unitPriceText = (0, import_core7.formatUnitPrice)(
597
- defaultVariant.unitPrice,
598
- defaultVariant.unitPriceMeasurement,
599
- currency
600
- );
601
- return unitPriceText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }) : null;
602
- })()
603
- ] }),
604
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
605
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
606
- "button",
607
- {
608
- "aria-label": `Decrease ${product.title}`,
609
- onClick: () => updateQuantity(
610
- product.id,
611
- defaultVariant.id,
612
- selected.quantity - 1
613
- ),
614
- children: "\u2212"
615
- }
616
- ),
617
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: selected.quantity }),
618
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
619
- "button",
620
- {
621
- "aria-label": `Increase ${product.title}`,
622
- onClick: () => updateQuantity(
623
- product.id,
624
- defaultVariant.id,
625
- selected.quantity + 1
626
- ),
627
- children: "+"
628
- }
629
- )
630
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
631
- "button",
632
- {
633
- className: "lb-bundle__select-btn",
634
- onClick: () => toggleProduct(product.id, defaultVariant),
635
- disabled: !defaultVariant.availableForSale,
636
- children: defaultVariant.availableForSale ? "Select" : "Sold out"
637
- }
638
- ) })
639
- ]
640
- },
641
- product.id
642
- );
643
- }) }),
644
- validation.message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
645
- cartError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
646
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
964
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
965
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
966
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
967
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
968
+ MixMatchProductRow,
969
+ {
970
+ bundle,
971
+ product,
972
+ currency,
973
+ selections,
974
+ onToggle: toggleProduct,
975
+ onUpdateQuantity: updateQuantity
976
+ },
977
+ product.id
978
+ )) }),
979
+ validation.message && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
980
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
981
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
647
982
  "button",
648
983
  {
649
984
  className: "lb-bundle__cta",
@@ -657,11 +992,132 @@ function MixMatchBundle(props) {
657
992
  }
658
993
  );
659
994
  }
995
+ function MixMatchProductRow({
996
+ bundle,
997
+ product,
998
+ currency,
999
+ selections,
1000
+ onToggle,
1001
+ onUpdateQuantity
1002
+ }) {
1003
+ const variants = product.variants.nodes;
1004
+ const optionNames = (0, import_react7.useMemo)(() => {
1005
+ const first = variants[0];
1006
+ return first ? first.selectedOptions.map((o) => o.name) : [];
1007
+ }, [variants]);
1008
+ const showPicker = variants.length > 1 && optionNames.length > 0;
1009
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1010
+ const displayVariant = selectedVariant ?? variants.find(
1011
+ (v) => (0, import_core9.isVariantFulfillable)(v, (0, import_core9.resolveBundleQty)(bundle, product.id, v.id))
1012
+ ) ?? variants[0] ?? null;
1013
+ if (!displayVariant) return null;
1014
+ const displayVariantQty = (0, import_core9.resolveBundleQty)(
1015
+ bundle,
1016
+ product.id,
1017
+ displayVariant.id
1018
+ );
1019
+ const displayVariantFulfillable = (0, import_core9.isVariantFulfillable)(
1020
+ displayVariant,
1021
+ displayVariantQty
1022
+ );
1023
+ const key = `${product.id}:${displayVariant.id}`;
1024
+ const selected = selections.get(key);
1025
+ const thumbImage = displayVariant.image ?? product.featuredImage;
1026
+ const unitPriceText = (0, import_core9.formatUnitPrice)(
1027
+ displayVariant.unitPrice,
1028
+ displayVariant.unitPriceMeasurement,
1029
+ currency
1030
+ );
1031
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1032
+ "div",
1033
+ {
1034
+ className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
1035
+ children: [
1036
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1037
+ "img",
1038
+ {
1039
+ src: thumbImage.url,
1040
+ alt: thumbImage.altText ?? product.title,
1041
+ className: "lb-bundle__product-image",
1042
+ loading: "lazy"
1043
+ }
1044
+ ),
1045
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
1046
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core9.formatMoney)(displayVariant.price.amount, currency) }),
1048
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1049
+ (0, import_core9.shouldShowLowStockBadge)(
1050
+ displayVariant,
1051
+ displayVariantQty,
1052
+ bundle.widgetConfig.lowStockThreshold,
1053
+ bundle.widgetConfig.showLowStockBadge
1054
+ ) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1055
+ "Only ",
1056
+ displayVariant.quantityAvailable,
1057
+ " left"
1058
+ ] }),
1059
+ showPicker && !selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1060
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1061
+ value: o.value,
1062
+ label: o.value,
1063
+ disabled: o.disabled
1064
+ }));
1065
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1066
+ VariantDropdown,
1067
+ {
1068
+ options: dropdownOptions,
1069
+ value: selectedValues[optionIndex] ?? null,
1070
+ onChange: (v) => setOptionValue(optionIndex, v),
1071
+ ariaLabel: optionName
1072
+ },
1073
+ optionName
1074
+ );
1075
+ }) })
1076
+ ] }),
1077
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1078
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1079
+ "button",
1080
+ {
1081
+ "aria-label": `Decrease ${product.title}`,
1082
+ onClick: () => onUpdateQuantity(
1083
+ product.id,
1084
+ displayVariant.id,
1085
+ selected.quantity - 1
1086
+ ),
1087
+ children: "\u2212"
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selected.quantity }),
1091
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1092
+ "button",
1093
+ {
1094
+ "aria-label": `Increase ${product.title}`,
1095
+ onClick: () => onUpdateQuantity(
1096
+ product.id,
1097
+ displayVariant.id,
1098
+ selected.quantity + 1
1099
+ ),
1100
+ children: "+"
1101
+ }
1102
+ )
1103
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1104
+ "button",
1105
+ {
1106
+ className: "lb-bundle__select-btn",
1107
+ onClick: () => onToggle(product.id, displayVariant),
1108
+ disabled: !displayVariantFulfillable,
1109
+ children: displayVariantFulfillable ? "Select" : "Sold out"
1110
+ }
1111
+ ) })
1112
+ ]
1113
+ }
1114
+ );
1115
+ }
660
1116
 
661
1117
  // src/components/VolumeBundle.tsx
662
- var import_react6 = require("react");
663
- var import_core8 = require("@lime-bundles/core");
664
- var import_jsx_runtime3 = require("react/jsx-runtime");
1118
+ var import_react8 = require("react");
1119
+ var import_core10 = require("@lime-bundles/core");
1120
+ var import_jsx_runtime4 = require("react/jsx-runtime");
665
1121
  function VolumeBundle(props) {
666
1122
  const {
667
1123
  shopDomain,
@@ -685,12 +1141,12 @@ function VolumeBundle(props) {
685
1141
  bundleType: "volume",
686
1142
  enabled: analyticsEnabled !== false
687
1143
  });
688
- const [quantity, setQuantity] = (0, import_react6.useState)(1);
689
- const [addingToCart, setAddingToCart] = (0, import_react6.useState)(false);
690
- const [cartError, setCartError] = (0, import_react6.useState)(null);
1144
+ const [quantity, setQuantity] = (0, import_react8.useState)(1);
1145
+ const [addingToCart, setAddingToCart] = (0, import_react8.useState)(false);
1146
+ const [cartError, setCartError] = (0, import_react8.useState)(null);
691
1147
  const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
692
1148
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
693
- (0, import_react6.useEffect)(() => {
1149
+ (0, import_react8.useEffect)(() => {
694
1150
  if (result.status === "error") {
695
1151
  onError?.(result.error);
696
1152
  return;
@@ -704,18 +1160,38 @@ function VolumeBundle(props) {
704
1160
  }
705
1161
  }, [result, onError]);
706
1162
  const product = bundle?.products[0];
707
- const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1163
+ const variants = (0, import_react8.useMemo)(() => product?.variants.nodes ?? [], [product]);
1164
+ const optionNames = (0, import_react8.useMemo)(() => {
1165
+ const first = variants[0];
1166
+ return first ? first.selectedOptions.map((o) => o.name) : [];
1167
+ }, [variants]);
1168
+ const showPicker = variants.length > 1 && optionNames.length > 0;
1169
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1170
+ const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
1171
+ const displayVariant = selectedVariant ?? variants.find((v) => (0, import_core10.isVariantFulfillable)(v, minTierQty)) ?? variants[0];
1172
+ const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
708
1173
  const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
709
- const tierSavings = bundle ? (0, import_core8.calculateTierSavings)(
710
- bundle.volumeTiers,
711
- basePrice,
712
- quantity,
713
- bundle.discountConfig.discountType
714
- ) : [];
715
- const activeTier = bundle ? (0, import_core8.getActiveTier)(bundle.volumeTiers, quantity) : null;
716
- const handleAddToCart = (0, import_react6.useCallback)(async () => {
1174
+ const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
1175
+ const unitPriceText = displayVariant ? (0, import_core10.formatUnitPrice)(
1176
+ displayVariant.unitPrice,
1177
+ displayVariant.unitPriceMeasurement,
1178
+ currency
1179
+ ) : null;
1180
+ const tierSavings = (0, import_react8.useMemo)(
1181
+ () => bundle ? (0, import_core10.calculateTierSavings)(
1182
+ bundle.volumeTiers,
1183
+ basePrice,
1184
+ quantity,
1185
+ bundle.discountConfig.discountType
1186
+ ) : [],
1187
+ [bundle, basePrice, quantity]
1188
+ );
1189
+ const activeTier = bundle ? (0, import_core10.getActiveTier)(bundle.volumeTiers, quantity) : null;
1190
+ const handleAddToCart = (0, import_react8.useCallback)(async () => {
717
1191
  if (!bundle || !product) return;
718
- const variant = product.variants.nodes.find((v) => v.availableForSale);
1192
+ const variant = selectedVariant ?? product.variants.nodes.find(
1193
+ (v) => (0, import_core10.isVariantFulfillable)(v, quantity)
1194
+ );
719
1195
  if (!variant) return;
720
1196
  const lines = [
721
1197
  {
@@ -752,18 +1228,20 @@ function VolumeBundle(props) {
752
1228
  basePrice,
753
1229
  onAddToCart,
754
1230
  onError,
755
- trackAddToCart
1231
+ trackAddToCart,
1232
+ selectedVariant,
1233
+ tierSavings
756
1234
  ]);
757
1235
  if (result.status === "loading") {
758
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
760
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
1236
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1237
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
1238
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
761
1239
  ] });
762
1240
  }
763
1241
  if (result.status === "error") return null;
764
1242
  if (!bundle) return null;
765
1243
  if (!product) return null;
766
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1244
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
767
1245
  "div",
768
1246
  {
769
1247
  ref: (el) => {
@@ -774,55 +1252,64 @@ function VolumeBundle(props) {
774
1252
  role: "region",
775
1253
  "aria-label": bundle.title,
776
1254
  children: [
777
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
778
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
779
- product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1255
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
1256
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
1257
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
780
1258
  "img",
781
1259
  {
782
- src: product.featuredImage.url,
783
- alt: product.featuredImage.altText ?? product.title,
1260
+ src: thumbImage.url,
1261
+ alt: thumbImage.altText ?? product.title,
784
1262
  className: "lb-bundle__product-image",
785
1263
  loading: "lazy"
786
1264
  }
787
1265
  ),
788
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
789
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
790
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "lb-bundle__product-price", children: [
791
- (0, import_core8.formatMoney)(basePrice, currency),
1266
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product-info", children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1268
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "lb-bundle__product-price", children: [
1269
+ (0, import_core10.formatMoney)(basePrice, currency),
792
1270
  " each"
793
1271
  ] }),
794
- (() => {
795
- const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
796
- const unitPriceText = variant ? (0, import_core8.formatUnitPrice)(
797
- variant.unitPrice,
798
- variant.unitPriceMeasurement,
799
- currency
800
- ) : null;
801
- return unitPriceText ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }) : null;
802
- })()
1272
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1273
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1274
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1275
+ value: o.value,
1276
+ label: o.value,
1277
+ disabled: o.disabled
1278
+ }));
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1280
+ VariantDropdown,
1281
+ {
1282
+ options: dropdownOptions,
1283
+ value: selectedValues[optionIndex] ?? null,
1284
+ onChange: (v) => setOptionValue(optionIndex, v),
1285
+ ariaLabel: optionName
1286
+ },
1287
+ optionName
1288
+ );
1289
+ }) })
803
1290
  ] })
804
1291
  ] }),
805
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1292
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
806
1293
  "div",
807
1294
  {
808
1295
  className: "lb-bundle__tiers",
809
1296
  role: "table",
810
1297
  "aria-label": "Volume discounts",
811
- children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1298
+ children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
812
1299
  "div",
813
1300
  {
814
1301
  className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
815
1302
  role: "row",
816
1303
  children: [
817
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
1304
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
818
1305
  ts.tier.minQuantity,
819
1306
  "+ items"
820
1307
  ] }),
821
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
822
- (0, import_core8.formatMoney)(ts.unitPrice, currency),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
1309
+ (0, import_core10.formatMoney)(ts.unitPrice, currency),
823
1310
  " each"
824
1311
  ] }),
825
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
1312
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
826
1313
  "Save ",
827
1314
  ts.savingsPercent.toFixed(0),
828
1315
  "%"
@@ -833,10 +1320,10 @@ function VolumeBundle(props) {
833
1320
  ))
834
1321
  }
835
1322
  ),
836
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
837
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
838
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
839
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1323
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
1324
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1326
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
840
1327
  "button",
841
1328
  {
842
1329
  "aria-label": "Decrease quantity",
@@ -844,7 +1331,7 @@ function VolumeBundle(props) {
844
1331
  children: "\u2212"
845
1332
  }
846
1333
  ),
847
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1334
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
848
1335
  "input",
849
1336
  {
850
1337
  id: `lb-qty-${bundle.id}`,
@@ -858,7 +1345,7 @@ function VolumeBundle(props) {
858
1345
  className: "lb-bundle__quantity-input"
859
1346
  }
860
1347
  ),
861
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1348
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
862
1349
  "button",
863
1350
  {
864
1351
  "aria-label": "Increase quantity",
@@ -868,8 +1355,18 @@ function VolumeBundle(props) {
868
1355
  )
869
1356
  ] })
870
1357
  ] }),
871
- cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
872
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1358
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1359
+ bundle && displayVariant && (0, import_core10.shouldShowLowStockBadge)(
1360
+ displayVariant,
1361
+ minTierQty,
1362
+ bundle.widgetConfig.lowStockThreshold,
1363
+ bundle.widgetConfig.showLowStockBadge
1364
+ ) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle-low-stock-badge", children: [
1365
+ "Only ",
1366
+ displayVariant.quantityAvailable,
1367
+ " left"
1368
+ ] }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
873
1370
  "button",
874
1371
  {
875
1372
  className: "lb-bundle__cta",
@@ -885,21 +1382,21 @@ function VolumeBundle(props) {
885
1382
  }
886
1383
 
887
1384
  // src/hooks/useBundlesForProduct.ts
888
- var import_react7 = require("react");
889
- var import_core9 = require("@lime-bundles/core");
1385
+ var import_react9 = require("react");
1386
+ var import_core11 = require("@lime-bundles/core");
890
1387
  var INITIAL_STATE2 = {
891
1388
  status: "loading",
892
1389
  bundles: null,
893
1390
  error: null
894
1391
  };
895
1392
  function useBundlesForProduct(options) {
896
- const [state, setState] = (0, import_react7.useState)(
1393
+ const [state, setState] = (0, import_react9.useState)(
897
1394
  INITIAL_STATE2
898
1395
  );
899
- (0, import_react7.useEffect)(() => {
1396
+ (0, import_react9.useEffect)(() => {
900
1397
  const controller = new AbortController();
901
1398
  setState(INITIAL_STATE2);
902
- (0, import_core9.fetchBundlesForProduct)({
1399
+ (0, import_core11.fetchBundlesForProduct)({
903
1400
  shopDomain: options.shopDomain,
904
1401
  storefrontAccessToken: options.storefrontAccessToken,
905
1402
  productHandle: options.productHandle,
@@ -922,7 +1419,7 @@ function useBundlesForProduct(options) {
922
1419
  signal: controller.signal
923
1420
  }).then((css) => {
924
1421
  if (controller.signal.aborted) return;
925
- (0, import_core9.injectCustomCss)(options.shopDomain, css);
1422
+ (0, import_core11.injectCustomCss)(options.shopDomain, css);
926
1423
  }).catch(() => {
927
1424
  });
928
1425
  return () => {
@@ -938,8 +1435,8 @@ function useBundlesForProduct(options) {
938
1435
  }
939
1436
 
940
1437
  // src/index.ts
941
- var import_core10 = require("@lime-bundles/core");
942
- var import_core11 = require("@lime-bundles/core");
1438
+ var import_core12 = require("@lime-bundles/core");
1439
+ var import_core13 = require("@lime-bundles/core");
943
1440
  // Annotate the CommonJS export names for ESM import in node:
944
1441
  0 && (module.exports = {
945
1442
  BUNDLES_FOR_PRODUCT_QUERY,
@@ -949,6 +1446,7 @@ var import_core11 = require("@lime-bundles/core");
949
1446
  MixMatchBundle,
950
1447
  SHOP_CUSTOM_CSS_QUERY,
951
1448
  StorefrontApiError,
1449
+ VariantDropdown,
952
1450
  VolumeBundle,
953
1451
  WIDGET_CONFIG_DEFAULTS,
954
1452
  applyABVariantB,
@@ -984,6 +1482,7 @@ var import_core11 = require("@lime-bundles/core");
984
1482
  useAnalytics,
985
1483
  useBundleData,
986
1484
  useBundlesForProduct,
1485
+ useVariantSelection,
987
1486
  useWidgetConfigVars,
988
1487
  validateQuantity
989
1488
  });