@lime-bundles/widget 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -508,7 +508,7 @@ function buildWidgetShell(typeClass, opts) {
508
508
  "data-countdown": ""
509
509
  });
510
510
  const label2 = box("span", "lb-bundle-countdown-label");
511
- label2.textContent = opts.countdownLabel ?? "Ends in";
511
+ label2.textContent = opts.countdownLabel ?? "Limited time offer";
512
512
  countdown.appendChild(label2);
513
513
  countdown.appendChild(
514
514
  box("span", "lb-bundle-countdown-timer", {
@@ -520,10 +520,12 @@ function buildWidgetShell(typeClass, opts) {
520
520
  const products = box("div", typeClass + "__products lb-edge-fade");
521
521
  root.appendChild(products);
522
522
  root.appendChild(box("div", "lb-bundle-divider"));
523
- const summary = box("div", "lb-bundle-summary");
523
+ const summary = box("div", "lb-bundle-summary", {
524
+ "data-pricing-section": ""
525
+ });
524
526
  const text = box("div", "lb-bundle-summary__text");
525
527
  const label = box("span", "lb-bundle-summary__label");
526
- label.textContent = opts.summaryLabel ?? "Bundle price";
528
+ label.textContent = opts.summaryLabel ?? "Bundle total";
527
529
  text.appendChild(label);
528
530
  if (opts.withItemCount) {
529
531
  label.appendChild(box("span", "", { "data-item-count": "" }));
@@ -554,6 +556,7 @@ function buildWidgetShell(typeClass, opts) {
554
556
  summary.appendChild(prices);
555
557
  root.appendChild(summary);
556
558
  const cta = box("button", "lb-bundle-cta", {
559
+ "data-cta-text": opts.ctaText,
557
560
  type: "button",
558
561
  "data-add-bundle": ""
559
562
  });
@@ -572,7 +575,9 @@ function buildWidgetShell(typeClass, opts) {
572
575
  );
573
576
  return { root, products, cta };
574
577
  }
575
- function buildVolumeTiers(tiers, groupLabel) {
578
+ function buildVolumeTiers(tiers, groupLabel, opts = {}) {
579
+ const showCompare = opts.showCompare !== false;
580
+ const showPriceEach = opts.showPriceEach !== false;
576
581
  const group = box("div", "lb-volume__tiers lb-edge-fade", {
577
582
  role: "radiogroup",
578
583
  "aria-label": groupLabel,
@@ -597,8 +602,17 @@ function buildVolumeTiers(tiers, groupLabel) {
597
602
  label.textContent = tier.label;
598
603
  info.appendChild(label);
599
604
  const price = box("span", "lb-volume__tier-price");
600
- price.appendChild(box("span", "lb-volume__tier-compare"));
601
- price.appendChild(box("span", "", { "data-tier-price-each": "" }));
605
+ if (showCompare && (tier.percent > 0 || tier.amountCents > 0)) {
606
+ price.appendChild(box("span", "lb-volume__tier-compare"));
607
+ }
608
+ if (showPriceEach) {
609
+ price.appendChild(box("span", "", { "data-tier-price-each": "" }));
610
+ if (opts.unitLabel) {
611
+ const unit = box("span", "lb-volume__tier-unit");
612
+ unit.textContent = opts.unitLabel;
613
+ price.appendChild(unit);
614
+ }
615
+ }
602
616
  info.appendChild(price);
603
617
  el2.appendChild(info);
604
618
  if (tier.badgeLabel || tier.savingsLabel) {
@@ -707,7 +721,10 @@ var DEFAULT_STRINGS = {
707
721
  required: "Required",
708
722
  soldOut: "Sold out",
709
723
  outOfStock: "Out of stock",
710
- itemsOutOfStock: "__COUNT__ items out of stock",
724
+ itemsOutOfStock: {
725
+ one: "__COUNT__ item out of stock",
726
+ other: "__COUNT__ items out of stock"
727
+ },
711
728
  quantity: "Quantity",
712
729
  increaseQuantity: "Increase quantity",
713
730
  decreaseQuantity: "Decrease quantity",
@@ -727,11 +744,26 @@ var DEFAULT_STRINGS = {
727
744
  stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
728
745
  each: " each",
729
746
  buyQty: "Buy __COUNT__",
747
+ itemCount: {
748
+ one: "__COUNT__ item",
749
+ other: "__COUNT__ items"
750
+ },
730
751
  needsSpots: {
731
752
  one: "Needs __COUNT__ spot",
732
753
  other: "Needs __COUNT__ spots"
733
754
  }
734
755
  };
756
+ function formatItemsOutOfStock(t, count) {
757
+ const raw = t.itemsOutOfStock;
758
+ let tpl;
759
+ if (raw && typeof raw === "object") {
760
+ const form = new Intl.PluralRules(t.locale || "en").select(count);
761
+ tpl = raw[form] || raw.other;
762
+ } else {
763
+ tpl = raw;
764
+ }
765
+ return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
766
+ }
735
767
 
736
768
  // ../render/src/dropdown/bind.ts
737
769
  import { dropdown } from "@lime-bundles/core";
@@ -1188,7 +1220,7 @@ function renderFixedBundle(container, bundle, outOfStockBehavior, onAddToCart, o
1188
1220
  shell.cta.disabled = true;
1189
1221
  const label = shell.cta.querySelector("[data-cta-label]");
1190
1222
  if (label) {
1191
- label.textContent = (t.itemsOutOfStock || "__COUNT__ items out of stock").split("__COUNT__").join(String(oosProducts.length));
1223
+ label.textContent = formatItemsOutOfStock(t, oosProducts.length);
1192
1224
  }
1193
1225
  } else {
1194
1226
  shell.cta.addEventListener("click", () => {
@@ -2553,17 +2585,17 @@ function buildPickerModal(opts) {
2553
2585
  }
2554
2586
 
2555
2587
  // src/renderers/mix-match.ts
2556
- function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
2588
+ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup, currentProductHandle) {
2557
2589
  const wc = bundle.widgetConfig;
2558
2590
  const t = DEFAULT_STRINGS;
2559
2591
  const requiredQty = bundle.minQuantity || 1;
2560
2592
  const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);
2561
2593
  const adapted = adaptProducts(bundle);
2562
- const eligibleProducts = adapted.map((p) => ({
2594
+ const eligibleProducts = adapted.map((p, i) => ({
2563
2595
  id: Number(p.productId),
2564
2596
  title: p.title ?? "",
2565
2597
  url: p.url ?? null,
2566
- type: "",
2598
+ type: bundle.products[i]?.productType ?? "",
2567
2599
  featuredImage: p.featuredImage ?? null,
2568
2600
  available: p.variants.some((v) => v.available),
2569
2601
  optionNames: p.optionNames,
@@ -2608,16 +2640,21 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2608
2640
  const overlay = buildPickerModal({
2609
2641
  bundleGid: bundle.id,
2610
2642
  domId: bundle.id.replace(/\D/g, ""),
2611
- showSearch: true,
2612
- showTypeFilters: false,
2643
+ showSearch: wc.showSearch !== false,
2644
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
2613
2645
  segmentCount: requiredQty,
2614
2646
  wizard: false
2615
2647
  });
2616
2648
  shell.root.appendChild(overlay);
2649
+ const currentIdx = currentProductHandle ? bundle.products.findIndex((p) => p.handle === currentProductHandle) : -1;
2617
2650
  const selectedItems = seedSelection(
2618
2651
  eligibleProducts,
2619
2652
  rulesMap,
2620
- requiredQty
2653
+ requiredQty,
2654
+ {
2655
+ enabled: !!currentProductHandle,
2656
+ currentProductId: currentIdx !== -1 ? eligibleProducts[currentIdx]?.id ?? null : null
2657
+ }
2621
2658
  );
2622
2659
  const rowCapacity = {
2623
2660
  remainingSpots: () => remainingUnits(selectedItems, requiredQty),
@@ -2654,7 +2691,13 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2654
2691
  shell.root,
2655
2692
  selectedItems,
2656
2693
  requiredQty,
2657
- bundle,
2694
+ // The shared updater reads the theme data-block shape: discount fields
2695
+ // flat, not nested under discountConfig. Passing the bundle itself left
2696
+ // `discountType` undefined, so the summary never painted.
2697
+ {
2698
+ discountType: bundle.discountConfig.discountType,
2699
+ discountValue: bundle.discountConfig.discountValue
2700
+ },
2658
2701
  (total, type, value) => calculateDiscount(total, type, value),
2659
2702
  updatePricing2(formatMoney)
2660
2703
  );
@@ -2667,7 +2710,7 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2667
2710
  rulesMap,
2668
2711
  eligibleProducts,
2669
2712
  requiredQty,
2670
- showQtySelector: true,
2713
+ showQtySelector: wc.mixMatchShowQuantitySelector !== false,
2671
2714
  items: selectedItems,
2672
2715
  formatMoney,
2673
2716
  rowCapacity,
@@ -2730,6 +2773,13 @@ function renderMixMatchBundle(container, bundle, outOfStockBehavior, onAddToCart
2730
2773
  onCleanup?.(bindDropdowns(shell.root));
2731
2774
  }
2732
2775
 
2776
+ // src/renderers/volume.ts
2777
+ import {
2778
+ bestValueTierIndex,
2779
+ resolveDefaultTierIndex,
2780
+ resolvePopularTierIndex
2781
+ } from "@lime-bundles/core";
2782
+
2733
2783
  // ../render/src/volume/pricing.ts
2734
2784
  function calcTierPrice(basePrice, discountType, tierEl) {
2735
2785
  if (discountType === "fixed_amount") {
@@ -2758,6 +2808,23 @@ function formatItemCount(qty, translations) {
2758
2808
  const tpl = translations.itemCount[form] || translations.itemCount.other || "__COUNT__";
2759
2809
  return " (" + tpl.split("__COUNT__").join(String(qty)) + ")";
2760
2810
  }
2811
+ function updateAllTierPrices(allTiers, basePrice, discountType, formatMoney) {
2812
+ for (let i = 0; i < allTiers.length; i++) {
2813
+ const tierEl = allTiers[i];
2814
+ const priceEachEl = tierEl.querySelector(
2815
+ "[data-tier-price-each]"
2816
+ );
2817
+ if (priceEachEl) {
2818
+ priceEachEl.textContent = formatMoney(
2819
+ calcTierPrice(basePrice, discountType, tierEl)
2820
+ );
2821
+ }
2822
+ const compareEl = tierEl.querySelector(
2823
+ ".lb-volume__tier-compare"
2824
+ );
2825
+ if (compareEl) compareEl.textContent = formatMoney(basePrice);
2826
+ }
2827
+ }
2761
2828
  function selectTier(container, allTiers, basePrice, discountType, index, translations, formatMoney) {
2762
2829
  for (let i = 0; i < allTiers.length; i++) {
2763
2830
  allTiers[i].setAttribute("aria-checked", i === index ? "true" : "false");
@@ -2815,8 +2882,17 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
2815
2882
  const discountType = bundle.discountConfig.discountType;
2816
2883
  const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);
2817
2884
  const t = DEFAULT_STRINGS;
2818
- const popularIdx = wc.popularBadge?.visible ? Math.floor((bundle.volumeTiers.length - 1) / 2) : -1;
2819
- const defaultIdx = 0;
2885
+ const bestIdx = bestValueTierIndex(bundle.volumeTiers, discountType);
2886
+ const defaultIdx = resolveDefaultTierIndex(
2887
+ wc.defaultTier,
2888
+ bundle.volumeTiers.length,
2889
+ bestIdx
2890
+ );
2891
+ const popularIdx = resolvePopularTierIndex(
2892
+ wc.popularBadge?.tierIndex,
2893
+ wc.popularBadge?.visible !== false,
2894
+ bestIdx
2895
+ );
2820
2896
  const tiers = bundle.volumeTiers.map((tier, i) => {
2821
2897
  const percent = discountType === "fixed_amount" ? 0 : Math.round(tier.percentage ?? 0);
2822
2898
  const amountCents = discountType === "fixed_amount" ? Math.round((tier.amount ?? 0) * 100) : 0;
@@ -2837,16 +2913,21 @@ function renderVolumeBundle(container, bundle, outOfStockBehavior, onAddToCart)
2837
2913
  title: bundle.title,
2838
2914
  subtitle: bundle.description,
2839
2915
  summaryLabel: "Total",
2840
- withItemCount: true,
2916
+ withItemCount: wc.pricing?.showItemCount !== false,
2841
2917
  totalPriceSlot: true,
2842
2918
  showSavingsBar: wc.savingsBar?.visible !== false,
2843
2919
  showComparePrice: wc.pricing?.showCompareAtPrice !== false,
2844
2920
  ctaText: wc.cta?.ctaText || t.addToCart || "Add to cart",
2845
2921
  endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt
2846
2922
  });
2847
- const group = buildVolumeTiers(tiers, "Select quantity");
2923
+ const group = buildVolumeTiers(tiers, "Select quantity", {
2924
+ showCompare: wc.pricing?.showComparePrice !== false,
2925
+ showPriceEach: wc.pricing?.showPerUnitPrice !== false,
2926
+ unitLabel: (t.each || "each").trim()
2927
+ });
2848
2928
  shell.products.replaceWith(group);
2849
2929
  const tierEls = group.querySelectorAll("[data-tier-index]");
2930
+ updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);
2850
2931
  let selectedIndex = defaultIdx;
2851
2932
  const apply = () => selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);
2852
2933
  group.addEventListener("click", (e) => {
@@ -2996,6 +3077,13 @@ function buildBogoCartItems(sides, percent, buyQty, getQty) {
2996
3077
  }
2997
3078
 
2998
3079
  // src/renderers/bogo.ts
3080
+ function buildBogoPlus() {
3081
+ const plus = document.createElement("div");
3082
+ plus.className = "lb-bogo__plus";
3083
+ plus.setAttribute("aria-hidden", "true");
3084
+ plus.innerHTML = '<svg width="12" height="12" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="9" y1="3" x2="9" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="9" x2="15" y2="9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
3085
+ return plus;
3086
+ }
2999
3087
  function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, onCleanup) {
3000
3088
  const wc = bundle.widgetConfig;
3001
3089
  const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);
@@ -3044,6 +3132,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3044
3132
  const badgeText = percent >= 100 ? "Free" : percent + "% off";
3045
3133
  sides.forEach((side, i) => {
3046
3134
  const isGet = i === 1;
3135
+ if (isGet) shell.products.appendChild(buildBogoPlus());
3047
3136
  const isOos = !side.variants.some((v) => v.available);
3048
3137
  const row = buildRowSkeleton(side, t, {
3049
3138
  isOos,
@@ -3072,7 +3161,7 @@ function renderBogoBundle(container, bundle, outOfStockBehavior, onAddToCart, on
3072
3161
  shell.cta.disabled = true;
3073
3162
  const label = shell.cta.querySelector("[data-cta-label]");
3074
3163
  if (label) {
3075
- label.textContent = (t.itemsOutOfStock || "__COUNT__ items out of stock").split("__COUNT__").join(String(oosCount));
3164
+ label.textContent = formatItemsOutOfStock(t, oosCount);
3076
3165
  }
3077
3166
  } else {
3078
3167
  shell.cta.addEventListener("click", () => {
@@ -3798,8 +3887,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3798
3887
  const overlay = buildPickerModal({
3799
3888
  bundleGid: bundle.id,
3800
3889
  domId: bundle.id.replace(/\D/g, ""),
3801
- showSearch: true,
3802
- showTypeFilters: true,
3890
+ showSearch: wc.showSearch !== false,
3891
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
3803
3892
  segmentCount: null,
3804
3893
  wizard: true
3805
3894
  });
@@ -3846,8 +3935,8 @@ function renderMultiStepBundle(container, bundle, outOfStockBehavior, onAddToCar
3846
3935
  selections,
3847
3936
  t,
3848
3937
  rulesMap,
3849
- showQtySelector: true,
3850
- showTypeFilters: true,
3938
+ showQtySelector: wc.mixMatchShowQuantitySelector !== false,
3939
+ showTypeFilters: wc.mixMatchShowTypeFilters !== false,
3851
3940
  formatMoney,
3852
3941
  dropdown: void 0,
3853
3942
  portalTarget: null,
@@ -6553,6 +6642,13 @@ var LimeBundleElement = class extends HTMLElement {
6553
6642
  buyer = void 0;
6554
6643
  shadow;
6555
6644
  bundles = [];
6645
+ /**
6646
+ * The product handle this embed is standing on, when one can be resolved
6647
+ * (attribute → meta tag → /products/<handle> path). The mix-and-match
6648
+ * courtesy seed uses it to pre-add the current product the way the theme
6649
+ * widget does; null on pages with no product context.
6650
+ */
6651
+ currentProductHandle = null;
6556
6652
  abortController = null;
6557
6653
  impressionCleanups = [];
6558
6654
  /**
@@ -6672,11 +6768,12 @@ var LimeBundleElement = class extends HTMLElement {
6672
6768
  try {
6673
6769
  let bundlePromise;
6674
6770
  let singleBundleMode = false;
6771
+ this.currentProductHandle = resolveProductHandle(this.productHandleAttr);
6675
6772
  if (this.bundleGid) {
6676
6773
  singleBundleMode = true;
6677
6774
  bundlePromise = this.fetchSingleBundle(client, controller.signal);
6678
6775
  } else {
6679
- const handle = resolveProductHandle(this.productHandleAttr);
6776
+ const handle = this.currentProductHandle;
6680
6777
  if (!handle) {
6681
6778
  this.teardownImpressions();
6682
6779
  this.renderError(
@@ -6890,6 +6987,7 @@ var LimeBundleElement = class extends HTMLElement {
6890
6987
  container.setAttribute("aria-label", bundle.title);
6891
6988
  container.setAttribute("data-bundle-type", bundle.bundleType);
6892
6989
  container.setAttribute("data-bundle-gid", bundle.id);
6990
+ if (bundle.endsAt) container.setAttribute("data-ends-at", bundle.endsAt);
6893
6991
  applyWidgetConfigVars(container, bundle.widgetConfig);
6894
6992
  this.renderCleanups.push(trackInputMode(container));
6895
6993
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
@@ -6910,7 +7008,8 @@ var LimeBundleElement = class extends HTMLElement {
6910
7008
  bundle,
6911
7009
  this.outOfStockBehavior,
6912
7010
  dispatch,
6913
- registerCleanup
7011
+ registerCleanup,
7012
+ this.currentProductHandle
6914
7013
  );
6915
7014
  break;
6916
7015
  case "volume":