@lime-bundles/react 2.3.0 → 2.4.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,14 @@ 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
644
  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);
645
+ const variant = selectedVariants[p.id] ?? p.variants.nodes.find((v) => v.availableForSale);
309
646
  return {
310
647
  productId: p.id,
311
648
  variant,
312
- quantity: (0, import_core6.resolveBundleQty)(bundle, p.id, variant.id)
649
+ quantity: (0, import_core8.resolveBundleQty)(bundle, p.id, variant.id)
313
650
  };
314
651
  }).filter((line) => line.quantity > 0);
315
652
  if (bundleLines.length === 0) return;
@@ -327,7 +664,7 @@ function FixedBundle(props) {
327
664
  await onAddToCart(lines);
328
665
  const totalPrice = bundleLines.reduce((sum, line) => {
329
666
  const price = parseFloat(line.variant.price.amount);
330
- const discounted = (0, import_core6.calculateDiscount)(
667
+ const discounted = (0, import_core8.calculateDiscount)(
331
668
  price,
332
669
  bundle.discountConfig.discountType,
333
670
  bundle.discountConfig.discountValue
@@ -349,17 +686,17 @@ function FixedBundle(props) {
349
686
  } finally {
350
687
  setAddingToCart(false);
351
688
  }
352
- }, [bundle, onAddToCart, onError, trackAddToCart]);
689
+ }, [bundle, onAddToCart, onError, trackAddToCart, selectedVariants]);
353
690
  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" })
691
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
692
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
693
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
357
694
  ] });
358
695
  }
359
696
  if (result.status === "error") return null;
360
697
  if (!bundle) return null;
361
698
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
362
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
699
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
363
700
  "div",
364
701
  {
365
702
  ref: (el) => {
@@ -370,38 +707,19 @@ function FixedBundle(props) {
370
707
  role: "region",
371
708
  "aria-label": bundle.title,
372
709
  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)(
710
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
711
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
712
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
713
+ FixedProductRow,
714
+ {
715
+ product,
716
+ currency,
717
+ onVariantChange: handleVariantChange
718
+ },
719
+ product.id
720
+ )) }),
721
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
722
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
405
723
  "button",
406
724
  {
407
725
  className: "lb-bundle__cta",
@@ -415,11 +733,67 @@ function FixedBundle(props) {
415
733
  }
416
734
  );
417
735
  }
736
+ function FixedProductRow({ product, currency, onVariantChange }) {
737
+ const variants = product.variants.nodes;
738
+ const optionNames = (0, import_react6.useMemo)(() => {
739
+ const first = variants[0];
740
+ return first ? first.selectedOptions.map((o) => o.name) : [];
741
+ }, [variants]);
742
+ const showPicker = variants.length > 1 && optionNames.length > 0;
743
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
744
+ variants,
745
+ optionNames
746
+ });
747
+ (0, import_react6.useEffect)(() => {
748
+ onVariantChange(product.id, selectedVariant ?? null);
749
+ }, [selectedVariant, product.id, onVariantChange]);
750
+ const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
751
+ const priceText = displayVariant ? (0, import_core8.formatMoney)(displayVariant.price.amount, currency) : (0, import_core8.formatMoney)(product.priceRange.minVariantPrice.amount, currency);
752
+ const unitPriceText = displayVariant ? (0, import_core8.formatUnitPrice)(
753
+ displayVariant.unitPrice,
754
+ displayVariant.unitPriceMeasurement,
755
+ currency
756
+ ) : null;
757
+ const thumbImage = displayVariant?.image ?? product.featuredImage;
758
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
759
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
760
+ "img",
761
+ {
762
+ src: thumbImage.url,
763
+ alt: thumbImage.altText ?? product.title,
764
+ className: "lb-bundle__product-image",
765
+ loading: "lazy"
766
+ }
767
+ ),
768
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
769
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
770
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: priceText }),
771
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
772
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
773
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
774
+ value: o.value,
775
+ label: o.value,
776
+ disabled: o.disabled
777
+ }));
778
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
779
+ VariantDropdown,
780
+ {
781
+ options: dropdownOptions,
782
+ value: selectedValues[optionIndex] ?? null,
783
+ onChange: (v) => setOptionValue(optionIndex, v),
784
+ ariaLabel: optionName
785
+ },
786
+ optionName
787
+ );
788
+ }) })
789
+ ] })
790
+ ] });
791
+ }
418
792
 
419
793
  // 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");
794
+ var import_react7 = require("react");
795
+ var import_core9 = require("@lime-bundles/core");
796
+ var import_jsx_runtime3 = require("react/jsx-runtime");
423
797
  function MixMatchBundle(props) {
424
798
  const {
425
799
  shopDomain,
@@ -443,14 +817,14 @@ function MixMatchBundle(props) {
443
817
  bundleType: "mix_match",
444
818
  enabled: analyticsEnabled !== false
445
819
  });
446
- const [selections, setSelections] = (0, import_react5.useState)(
820
+ const [selections, setSelections] = (0, import_react7.useState)(
447
821
  /* @__PURE__ */ new Map()
448
822
  );
449
- const [addingToCart, setAddingToCart] = (0, import_react5.useState)(false);
450
- const [cartError, setCartError] = (0, import_react5.useState)(null);
823
+ const [addingToCart, setAddingToCart] = (0, import_react7.useState)(false);
824
+ const [cartError, setCartError] = (0, import_react7.useState)(null);
451
825
  const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
452
826
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
453
- (0, import_react5.useEffect)(() => {
827
+ (0, import_react7.useEffect)(() => {
454
828
  if (result.status === "error") {
455
829
  onError?.(result.error);
456
830
  return;
@@ -467,8 +841,8 @@ function MixMatchBundle(props) {
467
841
  (sum, s) => sum + s.quantity,
468
842
  0
469
843
  );
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)(
844
+ const validation = bundle ? (0, import_core9.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
845
+ const toggleProduct = (0, import_react7.useCallback)(
472
846
  (productId, variant) => {
473
847
  setSelections((prev) => {
474
848
  const next = new Map(prev);
@@ -476,7 +850,7 @@ function MixMatchBundle(props) {
476
850
  if (next.has(key)) {
477
851
  next.delete(key);
478
852
  } else {
479
- const configuredQty = bundle ? (0, import_core7.resolveBundleQty)(bundle, productId, variant.id) : 1;
853
+ const configuredQty = bundle ? (0, import_core9.resolveBundleQty)(bundle, productId, variant.id) : 1;
480
854
  next.set(key, {
481
855
  productId,
482
856
  variantId: variant.id,
@@ -488,7 +862,7 @@ function MixMatchBundle(props) {
488
862
  },
489
863
  [bundle]
490
864
  );
491
- const updateQuantity = (0, import_react5.useCallback)(
865
+ const updateQuantity = (0, import_react7.useCallback)(
492
866
  (productId, variantId, quantity) => {
493
867
  setSelections((prev) => {
494
868
  const next = new Map(prev);
@@ -503,7 +877,7 @@ function MixMatchBundle(props) {
503
877
  },
504
878
  []
505
879
  );
506
- const handleAddToCart = (0, import_react5.useCallback)(async () => {
880
+ const handleAddToCart = (0, import_react7.useCallback)(async () => {
507
881
  if (!bundle || !validation.valid) return;
508
882
  const lines = Array.from(selections.values()).map((s) => ({
509
883
  merchandiseId: s.variantId,
@@ -548,15 +922,15 @@ function MixMatchBundle(props) {
548
922
  totalQuantity
549
923
  ]);
550
924
  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" })
925
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
926
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
927
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
554
928
  ] });
555
929
  }
556
930
  if (result.status === "error") return null;
557
931
  if (!bundle) return null;
558
932
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
559
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
933
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
560
934
  "div",
561
935
  {
562
936
  ref: (el) => {
@@ -567,83 +941,23 @@ function MixMatchBundle(props) {
567
941
  role: "region",
568
942
  "aria-label": bundle.title,
569
943
  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)(
944
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
945
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
946
+ /* @__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" }),
947
+ /* @__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)(
948
+ MixMatchProductRow,
949
+ {
950
+ product,
951
+ currency,
952
+ selections,
953
+ onToggle: toggleProduct,
954
+ onUpdateQuantity: updateQuantity
955
+ },
956
+ product.id
957
+ )) }),
958
+ validation.message && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
959
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
960
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
647
961
  "button",
648
962
  {
649
963
  className: "lb-bundle__cta",
@@ -657,11 +971,110 @@ function MixMatchBundle(props) {
657
971
  }
658
972
  );
659
973
  }
974
+ function MixMatchProductRow({
975
+ product,
976
+ currency,
977
+ selections,
978
+ onToggle,
979
+ onUpdateQuantity
980
+ }) {
981
+ const variants = product.variants.nodes;
982
+ const optionNames = (0, import_react7.useMemo)(() => {
983
+ const first = variants[0];
984
+ return first ? first.selectedOptions.map((o) => o.name) : [];
985
+ }, [variants]);
986
+ const showPicker = variants.length > 1 && optionNames.length > 0;
987
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
988
+ const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
989
+ if (!displayVariant) return null;
990
+ const key = `${product.id}:${displayVariant.id}`;
991
+ const selected = selections.get(key);
992
+ const thumbImage = displayVariant.image ?? product.featuredImage;
993
+ const unitPriceText = (0, import_core9.formatUnitPrice)(
994
+ displayVariant.unitPrice,
995
+ displayVariant.unitPriceMeasurement,
996
+ currency
997
+ );
998
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
999
+ "div",
1000
+ {
1001
+ className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
1002
+ children: [
1003
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1004
+ "img",
1005
+ {
1006
+ src: thumbImage.url,
1007
+ alt: thumbImage.altText ?? product.title,
1008
+ className: "lb-bundle__product-image",
1009
+ loading: "lazy"
1010
+ }
1011
+ ),
1012
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
1013
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1014
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core9.formatMoney)(displayVariant.price.amount, currency) }),
1015
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1016
+ showPicker && !selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1017
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1018
+ value: o.value,
1019
+ label: o.value,
1020
+ disabled: o.disabled
1021
+ }));
1022
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1023
+ VariantDropdown,
1024
+ {
1025
+ options: dropdownOptions,
1026
+ value: selectedValues[optionIndex] ?? null,
1027
+ onChange: (v) => setOptionValue(optionIndex, v),
1028
+ ariaLabel: optionName
1029
+ },
1030
+ optionName
1031
+ );
1032
+ }) })
1033
+ ] }),
1034
+ /* @__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: [
1035
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1036
+ "button",
1037
+ {
1038
+ "aria-label": `Decrease ${product.title}`,
1039
+ onClick: () => onUpdateQuantity(
1040
+ product.id,
1041
+ displayVariant.id,
1042
+ selected.quantity - 1
1043
+ ),
1044
+ children: "\u2212"
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selected.quantity }),
1048
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1049
+ "button",
1050
+ {
1051
+ "aria-label": `Increase ${product.title}`,
1052
+ onClick: () => onUpdateQuantity(
1053
+ product.id,
1054
+ displayVariant.id,
1055
+ selected.quantity + 1
1056
+ ),
1057
+ children: "+"
1058
+ }
1059
+ )
1060
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1061
+ "button",
1062
+ {
1063
+ className: "lb-bundle__select-btn",
1064
+ onClick: () => onToggle(product.id, displayVariant),
1065
+ disabled: !displayVariant.availableForSale,
1066
+ children: displayVariant.availableForSale ? "Select" : "Sold out"
1067
+ }
1068
+ ) })
1069
+ ]
1070
+ }
1071
+ );
1072
+ }
660
1073
 
661
1074
  // 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");
1075
+ var import_react8 = require("react");
1076
+ var import_core10 = require("@lime-bundles/core");
1077
+ var import_jsx_runtime4 = require("react/jsx-runtime");
665
1078
  function VolumeBundle(props) {
666
1079
  const {
667
1080
  shopDomain,
@@ -685,12 +1098,12 @@ function VolumeBundle(props) {
685
1098
  bundleType: "volume",
686
1099
  enabled: analyticsEnabled !== false
687
1100
  });
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);
1101
+ const [quantity, setQuantity] = (0, import_react8.useState)(1);
1102
+ const [addingToCart, setAddingToCart] = (0, import_react8.useState)(false);
1103
+ const [cartError, setCartError] = (0, import_react8.useState)(null);
691
1104
  const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
692
1105
  const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
693
- (0, import_react6.useEffect)(() => {
1106
+ (0, import_react8.useEffect)(() => {
694
1107
  if (result.status === "error") {
695
1108
  onError?.(result.error);
696
1109
  return;
@@ -704,18 +1117,35 @@ function VolumeBundle(props) {
704
1117
  }
705
1118
  }, [result, onError]);
706
1119
  const product = bundle?.products[0];
707
- const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
1120
+ const variants = (0, import_react8.useMemo)(() => product?.variants.nodes ?? [], [product]);
1121
+ const optionNames = (0, import_react8.useMemo)(() => {
1122
+ const first = variants[0];
1123
+ return first ? first.selectedOptions.map((o) => o.name) : [];
1124
+ }, [variants]);
1125
+ const showPicker = variants.length > 1 && optionNames.length > 0;
1126
+ const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
1127
+ const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
1128
+ const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
708
1129
  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 () => {
1130
+ const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
1131
+ const unitPriceText = displayVariant ? (0, import_core10.formatUnitPrice)(
1132
+ displayVariant.unitPrice,
1133
+ displayVariant.unitPriceMeasurement,
1134
+ currency
1135
+ ) : null;
1136
+ const tierSavings = (0, import_react8.useMemo)(
1137
+ () => bundle ? (0, import_core10.calculateTierSavings)(
1138
+ bundle.volumeTiers,
1139
+ basePrice,
1140
+ quantity,
1141
+ bundle.discountConfig.discountType
1142
+ ) : [],
1143
+ [bundle, basePrice, quantity]
1144
+ );
1145
+ const activeTier = bundle ? (0, import_core10.getActiveTier)(bundle.volumeTiers, quantity) : null;
1146
+ const handleAddToCart = (0, import_react8.useCallback)(async () => {
717
1147
  if (!bundle || !product) return;
718
- const variant = product.variants.nodes.find((v) => v.availableForSale);
1148
+ const variant = selectedVariant ?? product.variants.nodes.find((v) => v.availableForSale);
719
1149
  if (!variant) return;
720
1150
  const lines = [
721
1151
  {
@@ -752,18 +1182,20 @@ function VolumeBundle(props) {
752
1182
  basePrice,
753
1183
  onAddToCart,
754
1184
  onError,
755
- trackAddToCart
1185
+ trackAddToCart,
1186
+ selectedVariant,
1187
+ tierSavings
756
1188
  ]);
757
1189
  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" })
1190
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
1191
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
1192
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
761
1193
  ] });
762
1194
  }
763
1195
  if (result.status === "error") return null;
764
1196
  if (!bundle) return null;
765
1197
  if (!product) return null;
766
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1198
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
767
1199
  "div",
768
1200
  {
769
1201
  ref: (el) => {
@@ -774,55 +1206,64 @@ function VolumeBundle(props) {
774
1206
  role: "region",
775
1207
  "aria-label": bundle.title,
776
1208
  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)(
1209
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
1211
+ thumbImage && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
780
1212
  "img",
781
1213
  {
782
- src: product.featuredImage.url,
783
- alt: product.featuredImage.altText ?? product.title,
1214
+ src: thumbImage.url,
1215
+ alt: thumbImage.altText ?? product.title,
784
1216
  className: "lb-bundle__product-image",
785
1217
  loading: "lazy"
786
1218
  }
787
1219
  ),
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),
1220
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__product-info", children: [
1221
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "lb-bundle__product-price", children: [
1223
+ (0, import_core10.formatMoney)(basePrice, currency),
792
1224
  " each"
793
1225
  ] }),
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
- })()
1226
+ unitPriceText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
1227
+ showPicker && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
1228
+ const dropdownOptions = optionsFor(optionIndex).map((o) => ({
1229
+ value: o.value,
1230
+ label: o.value,
1231
+ disabled: o.disabled
1232
+ }));
1233
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1234
+ VariantDropdown,
1235
+ {
1236
+ options: dropdownOptions,
1237
+ value: selectedValues[optionIndex] ?? null,
1238
+ onChange: (v) => setOptionValue(optionIndex, v),
1239
+ ariaLabel: optionName
1240
+ },
1241
+ optionName
1242
+ );
1243
+ }) })
803
1244
  ] })
804
1245
  ] }),
805
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1246
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
806
1247
  "div",
807
1248
  {
808
1249
  className: "lb-bundle__tiers",
809
1250
  role: "table",
810
1251
  "aria-label": "Volume discounts",
811
- children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1252
+ children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
812
1253
  "div",
813
1254
  {
814
1255
  className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
815
1256
  role: "row",
816
1257
  children: [
817
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
1258
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
818
1259
  ts.tier.minQuantity,
819
1260
  "+ items"
820
1261
  ] }),
821
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
822
- (0, import_core8.formatMoney)(ts.unitPrice, currency),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
1263
+ (0, import_core10.formatMoney)(ts.unitPrice, currency),
823
1264
  " each"
824
1265
  ] }),
825
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
1266
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
826
1267
  "Save ",
827
1268
  ts.savingsPercent.toFixed(0),
828
1269
  "%"
@@ -833,10 +1274,10 @@ function VolumeBundle(props) {
833
1274
  ))
834
1275
  }
835
1276
  ),
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)(
1277
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
1278
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
1279
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
840
1281
  "button",
841
1282
  {
842
1283
  "aria-label": "Decrease quantity",
@@ -844,7 +1285,7 @@ function VolumeBundle(props) {
844
1285
  children: "\u2212"
845
1286
  }
846
1287
  ),
847
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1288
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
848
1289
  "input",
849
1290
  {
850
1291
  id: `lb-qty-${bundle.id}`,
@@ -858,7 +1299,7 @@ function VolumeBundle(props) {
858
1299
  className: "lb-bundle__quantity-input"
859
1300
  }
860
1301
  ),
861
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1302
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
862
1303
  "button",
863
1304
  {
864
1305
  "aria-label": "Increase quantity",
@@ -868,8 +1309,8 @@ function VolumeBundle(props) {
868
1309
  )
869
1310
  ] })
870
1311
  ] }),
871
- cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
872
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1312
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
873
1314
  "button",
874
1315
  {
875
1316
  className: "lb-bundle__cta",
@@ -885,21 +1326,21 @@ function VolumeBundle(props) {
885
1326
  }
886
1327
 
887
1328
  // src/hooks/useBundlesForProduct.ts
888
- var import_react7 = require("react");
889
- var import_core9 = require("@lime-bundles/core");
1329
+ var import_react9 = require("react");
1330
+ var import_core11 = require("@lime-bundles/core");
890
1331
  var INITIAL_STATE2 = {
891
1332
  status: "loading",
892
1333
  bundles: null,
893
1334
  error: null
894
1335
  };
895
1336
  function useBundlesForProduct(options) {
896
- const [state, setState] = (0, import_react7.useState)(
1337
+ const [state, setState] = (0, import_react9.useState)(
897
1338
  INITIAL_STATE2
898
1339
  );
899
- (0, import_react7.useEffect)(() => {
1340
+ (0, import_react9.useEffect)(() => {
900
1341
  const controller = new AbortController();
901
1342
  setState(INITIAL_STATE2);
902
- (0, import_core9.fetchBundlesForProduct)({
1343
+ (0, import_core11.fetchBundlesForProduct)({
903
1344
  shopDomain: options.shopDomain,
904
1345
  storefrontAccessToken: options.storefrontAccessToken,
905
1346
  productHandle: options.productHandle,
@@ -922,7 +1363,7 @@ function useBundlesForProduct(options) {
922
1363
  signal: controller.signal
923
1364
  }).then((css) => {
924
1365
  if (controller.signal.aborted) return;
925
- (0, import_core9.injectCustomCss)(options.shopDomain, css);
1366
+ (0, import_core11.injectCustomCss)(options.shopDomain, css);
926
1367
  }).catch(() => {
927
1368
  });
928
1369
  return () => {
@@ -938,8 +1379,8 @@ function useBundlesForProduct(options) {
938
1379
  }
939
1380
 
940
1381
  // src/index.ts
941
- var import_core10 = require("@lime-bundles/core");
942
- var import_core11 = require("@lime-bundles/core");
1382
+ var import_core12 = require("@lime-bundles/core");
1383
+ var import_core13 = require("@lime-bundles/core");
943
1384
  // Annotate the CommonJS export names for ESM import in node:
944
1385
  0 && (module.exports = {
945
1386
  BUNDLES_FOR_PRODUCT_QUERY,
@@ -949,6 +1390,7 @@ var import_core11 = require("@lime-bundles/core");
949
1390
  MixMatchBundle,
950
1391
  SHOP_CUSTOM_CSS_QUERY,
951
1392
  StorefrontApiError,
1393
+ VariantDropdown,
952
1394
  VolumeBundle,
953
1395
  WIDGET_CONFIG_DEFAULTS,
954
1396
  applyABVariantB,
@@ -984,6 +1426,7 @@ var import_core11 = require("@lime-bundles/core");
984
1426
  useAnalytics,
985
1427
  useBundleData,
986
1428
  useBundlesForProduct,
1429
+ useVariantSelection,
987
1430
  useWidgetConfigVars,
988
1431
  validateQuantity
989
1432
  });