@lime-bundles/widget 2.1.0 → 2.2.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.cjs CHANGED
@@ -25,6 +25,9 @@ __export(index_exports, {
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
27
  // src/lime-bundle.ts
28
+ var import_core5 = require("@lime-bundles/core");
29
+
30
+ // src/renderers/fixed.ts
28
31
  var import_core4 = require("@lime-bundles/core");
29
32
 
30
33
  // src/renderers/pricing.ts
@@ -75,6 +78,32 @@ function parseIso(iso) {
75
78
  return Number.isFinite(t) ? t : null;
76
79
  }
77
80
 
81
+ // src/renderers/cta-button.ts
82
+ function buildCtaButton(label) {
83
+ const button = document.createElement("button");
84
+ button.type = "button";
85
+ button.className = "lb-bundle-cta";
86
+ button.setAttribute("data-add-bundle", "");
87
+ const labelSpan = document.createElement("span");
88
+ labelSpan.className = "lb-cta-label";
89
+ labelSpan.setAttribute("data-cta-label", "");
90
+ labelSpan.textContent = label;
91
+ button.appendChild(labelSpan);
92
+ const spinnerSpan = document.createElement("span");
93
+ spinnerSpan.className = "lb-cta-spinner";
94
+ spinnerSpan.setAttribute("data-cta-spinner", "");
95
+ spinnerSpan.setAttribute("aria-hidden", "true");
96
+ spinnerSpan.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 2a10 10 0 0 1 10 10" /></svg>';
97
+ button.appendChild(spinnerSpan);
98
+ return button;
99
+ }
100
+ function setCtaLabel(button, text) {
101
+ const label = button.querySelector("[data-cta-label]");
102
+ if (label) {
103
+ label.textContent = text;
104
+ }
105
+ }
106
+
78
107
  // src/renderers/dom.ts
79
108
  function el(tag, className, attrs = {}) {
80
109
  const node = document.createElement(tag);
@@ -97,11 +126,7 @@ var PLACEHOLDER_THUMB_SVG = `
97
126
  </svg>`;
98
127
  function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
99
128
  const wc = bundle.widgetConfig;
100
- const qtyFor = (productId, variantId) => {
101
- const vq = bundle.variantQuantities[variantId];
102
- if (vq !== void 0) return vq;
103
- return bundle.productQuantities[productId] ?? 1;
104
- };
129
+ const qtyFor = (productId, variantId) => (0, import_core4.resolveBundleQty)(bundle, productId, variantId);
105
130
  const rows = [];
106
131
  let oosCount = 0;
107
132
  bundle.products.forEach((product, idx) => {
@@ -192,9 +217,7 @@ function buildRowState(bundle, product, productIndex) {
192
217
  const eligibleVariants = selectedVariantIds && selectedVariantIds.length > 0 ? available.filter((v) => selectedVariantIds.includes(v.id)) : available;
193
218
  const isOos = eligibleVariants.length === 0;
194
219
  const selected = eligibleVariants[0] ?? null;
195
- const productQty = bundle.productQuantities[product.id] ?? 1;
196
- const variantQty = selected ? bundle.variantQuantities[selected.id] : void 0;
197
- const qty = variantQty ?? productQty;
220
+ const qty = selected ? (0, import_core4.resolveBundleQty)(bundle, product.id, selected.id) : 1;
198
221
  return { product, eligibleVariants, selected, qty, isOos };
199
222
  }
200
223
  function renderHeader(bundle, currency) {
@@ -412,15 +435,11 @@ function renderSavingsBar() {
412
435
  };
413
436
  }
414
437
  function renderCta(bundle, oosCount, onClick) {
415
- const button = document.createElement("button");
416
- button.type = "button";
417
- button.className = "lb-bundle-cta";
418
- button.setAttribute("data-add-bundle", "");
438
+ const label = oosCount > 0 ? `${oosCount} item${oosCount === 1 ? "" : "s"} out of stock` : bundle.widgetConfig.cta.ctaText || "Add to cart";
439
+ const button = buildCtaButton(label);
419
440
  if (oosCount > 0) {
420
441
  button.disabled = true;
421
- button.textContent = `${oosCount} item${oosCount === 1 ? "" : "s"} out of stock`;
422
442
  } else {
423
- button.textContent = bundle.widgetConfig.cta.ctaText || "Add to cart";
424
443
  button.addEventListener("click", () => {
425
444
  if (button.disabled) return;
426
445
  onClick();
@@ -517,12 +536,8 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
517
536
  isOverMax: () => selections.length >= maxQty
518
537
  });
519
538
  root.appendChild(modal.el);
520
- const cta = document.createElement("button");
521
- cta.type = "button";
522
- cta.className = "lb-bundle-cta";
523
- cta.setAttribute("data-add-bundle", "");
539
+ const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
524
540
  cta.disabled = true;
525
- cta.textContent = `Select ${requiredQty} items to unlock`;
526
541
  cta.addEventListener("click", () => {
527
542
  if (cta.disabled) return;
528
543
  const lines = selections.map((s) => ({
@@ -615,10 +630,10 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
615
630
  const count = selections.length;
616
631
  if (count < requiredQty) {
617
632
  cta.disabled = true;
618
- cta.textContent = `Select ${requiredQty - count} more to unlock`;
633
+ setCtaLabel(cta, `Select ${requiredQty - count} more to unlock`);
619
634
  } else {
620
635
  cta.disabled = false;
621
- cta.textContent = wc.cta.ctaText || "Add to cart";
636
+ setCtaLabel(cta, wc.cta.ctaText || "Add to cart");
622
637
  }
623
638
  }
624
639
  }
@@ -1353,12 +1368,9 @@ function renderSavingsBar3(resolved, selectedIndex, currency) {
1353
1368
  function renderCta2(bundle, onClick) {
1354
1369
  const product = bundle.products[0];
1355
1370
  const isAvailable = product?.variants.nodes.some((v) => v.availableForSale);
1356
- const button = document.createElement("button");
1357
- button.type = "button";
1358
- button.className = "lb-bundle-cta";
1359
- button.setAttribute("data-add-bundle", "");
1371
+ const label = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
1372
+ const button = buildCtaButton(label);
1360
1373
  if (!isAvailable) button.disabled = true;
1361
- button.textContent = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
1362
1374
  button.addEventListener("click", () => {
1363
1375
  if (button.disabled) return;
1364
1376
  onClick();
@@ -1397,7 +1409,7 @@ function clamp(n, min, max) {
1397
1409
  }
1398
1410
 
1399
1411
  // src/lime-bundle.ts
1400
- var import_core5 = require("@lime-bundles/core");
1412
+ var import_core6 = require("@lime-bundles/core");
1401
1413
 
1402
1414
  // src/styles/bundle-css.ts
1403
1415
  var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle widget types */
@@ -1472,11 +1484,20 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1472
1484
  display: none;
1473
1485
  }
1474
1486
 
1475
- /* Adjacent widget spacing \u2014 separates multiple bundles on the same product page */
1476
- .lb-bundle-widget + .lb-bundle-widget {
1487
+ /* Sibling widget spacing \u2014 separates multiple bundles on the same product page.
1488
+ Uses \`~\` (general sibling) rather than \`+\` (adjacent) because each Liquid
1489
+ loop iteration emits a {% style %} block before its widget div, so the
1490
+ rendered DOM alternates <style><widget><style><widget>. The \`+\` combinator
1491
+ requires immediate adjacency and would match nothing; \`~\` matches every
1492
+ widget after the first regardless of elements between. Single-widget pages
1493
+ stay unaffected (no prior \`.lb-bundle-widget\` sibling to match against).
1494
+ Only \`margin-top\` \u2014 do NOT override \`padding-top\` here. The gradient header
1495
+ uses \`margin-top: calc(-1 * var(--lb-widget-pad))\` to reach the widget's
1496
+ inner border edge, assuming padding-top == --lb-widget-pad. Changing
1497
+ padding-top on the subsequent widget breaks that math and leaves a visible
1498
+ gap above the header. */
1499
+ .lb-bundle-widget ~ .lb-bundle-widget {
1477
1500
  margin-top: 24px;
1478
- padding-top: 24px;
1479
- border-top: 1px solid var(--lb-border);
1480
1501
  }
1481
1502
 
1482
1503
  /* Header \u2014 gradient banner with title + savings badge */
@@ -1745,9 +1766,16 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1745
1766
  display: none;
1746
1767
  }
1747
1768
 
1748
- /* CTA button */
1769
+ /* CTA button.
1770
+ * Label and spinner share a single 1\xD71 grid cell so the button's intrinsic
1771
+ * width/height stays fixed when swapping between them \u2014 no layout shift when
1772
+ * entering the loading state. Visibility (not display) is used so the hidden
1773
+ * child still contributes to the cell's min-content sizing. See the
1774
+ * [data-loading="true"] rules below. */
1749
1775
  .lb-bundle-cta {
1750
- display: block;
1776
+ display: grid;
1777
+ grid-template-rows: 1fr;
1778
+ grid-template-columns: 1fr;
1751
1779
  width: 100%;
1752
1780
  padding: 16px;
1753
1781
  border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);
@@ -1776,12 +1804,51 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1776
1804
  cursor: not-allowed;
1777
1805
  }
1778
1806
 
1807
+ /* Both the label and the spinner occupy grid cell (1, 1). Only one is
1808
+ * visible at a time; the other keeps its box for sizing but is invisible. */
1809
+ .lb-cta-label,
1810
+ .lb-cta-spinner {
1811
+ grid-row: 1;
1812
+ grid-column: 1;
1813
+ display: flex;
1814
+ align-items: center;
1815
+ justify-content: center;
1816
+ min-width: 0;
1817
+ }
1818
+
1819
+ .lb-cta-spinner {
1820
+ visibility: hidden;
1821
+ }
1822
+
1823
+ .lb-cta-spinner svg {
1824
+ width: 20px;
1825
+ height: 20px;
1826
+ animation: lb-cta-spin 0.8s linear infinite;
1827
+ }
1828
+
1829
+ @keyframes lb-cta-spin {
1830
+ to { transform: rotate(360deg); }
1831
+ }
1832
+
1779
1833
  .lb-bundle-cta[data-loading="true"] {
1780
- opacity: 0.7;
1781
1834
  cursor: wait;
1782
1835
  pointer-events: none;
1783
1836
  }
1784
1837
 
1838
+ .lb-bundle-cta[data-loading="true"] .lb-cta-label {
1839
+ visibility: hidden;
1840
+ }
1841
+
1842
+ .lb-bundle-cta[data-loading="true"] .lb-cta-spinner {
1843
+ visibility: visible;
1844
+ }
1845
+
1846
+ @media (prefers-reduced-motion: reduce) {
1847
+ .lb-cta-spinner svg {
1848
+ animation-duration: 2.5s;
1849
+ }
1850
+ }
1851
+
1785
1852
  /* Error message */
1786
1853
  .lb-bundle-error {
1787
1854
  font-size: 16px;
@@ -2687,7 +2754,7 @@ var LimeBundleElement = class extends HTMLElement {
2687
2754
  const controller = new AbortController();
2688
2755
  this.abortController = controller;
2689
2756
  this.renderLoading();
2690
- const client = (0, import_core4.createStorefrontClient)({
2757
+ const client = (0, import_core5.createStorefrontClient)({
2691
2758
  shopDomain: this.shopDomain,
2692
2759
  accessToken: this.storefrontToken
2693
2760
  });
@@ -2712,7 +2779,7 @@ var LimeBundleElement = class extends HTMLElement {
2712
2779
  handle
2713
2780
  );
2714
2781
  }
2715
- const cssPromise = client.query(import_core4.SHOP_CUSTOM_CSS_QUERY, void 0, {
2782
+ const cssPromise = client.query(import_core5.SHOP_CUSTOM_CSS_QUERY, void 0, {
2716
2783
  signal: controller.signal
2717
2784
  }).catch(() => null);
2718
2785
  await bundlePromise;
@@ -2724,8 +2791,8 @@ var LimeBundleElement = class extends HTMLElement {
2724
2791
  }
2725
2792
  const css = await cssPromise;
2726
2793
  if (css?.shop?.metafield?.value) {
2727
- (0, import_core4.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
2728
- const sanitized = (0, import_core4.sanitizeCustomCss)(css.shop.metafield.value);
2794
+ (0, import_core5.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
2795
+ const sanitized = (0, import_core5.sanitizeCustomCss)(css.shop.metafield.value);
2729
2796
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
2730
2797
  }
2731
2798
  await this.applyABVariants();
@@ -2753,14 +2820,14 @@ var LimeBundleElement = class extends HTMLElement {
2753
2820
  this.bundles.map(async (bundle) => {
2754
2821
  if (!bundle.abTestId || !bundle.abVariantB) return bundle;
2755
2822
  try {
2756
- const assignment = await (0, import_core4.getABTestAssignment)(
2823
+ const assignment = await (0, import_core5.getABTestAssignment)(
2757
2824
  this.appUrl,
2758
2825
  this.shopDomain,
2759
2826
  bundle.abTestId,
2760
2827
  bundle.id
2761
2828
  );
2762
2829
  if (assignment?.variant === "B") {
2763
- return (0, import_core4.applyABVariantB)(bundle);
2830
+ return (0, import_core5.applyABVariantB)(bundle);
2764
2831
  }
2765
2832
  } catch (err) {
2766
2833
  console.warn(
@@ -2775,7 +2842,7 @@ var LimeBundleElement = class extends HTMLElement {
2775
2842
  }
2776
2843
  async fetchSingleBundle(client, signal) {
2777
2844
  const data = await client.query(
2778
- import_core4.BUNDLE_METAOBJECT_QUERY,
2845
+ import_core5.BUNDLE_METAOBJECT_QUERY,
2779
2846
  { id: this.bundleGid },
2780
2847
  { signal }
2781
2848
  );
@@ -2783,7 +2850,7 @@ var LimeBundleElement = class extends HTMLElement {
2783
2850
  this.bundles = [];
2784
2851
  return;
2785
2852
  }
2786
- const parsed = (0, import_core4.parseMetaobjectBundle)(
2853
+ const parsed = (0, import_core5.parseMetaobjectBundle)(
2787
2854
  data.metaobject.id,
2788
2855
  data.metaobject.fields
2789
2856
  );
@@ -2791,7 +2858,7 @@ var LimeBundleElement = class extends HTMLElement {
2791
2858
  }
2792
2859
  async fetchProductBundles(client, signal, productHandle) {
2793
2860
  const data = await client.query(
2794
- import_core4.BUNDLES_FOR_PRODUCT_QUERY,
2861
+ import_core5.BUNDLES_FOR_PRODUCT_QUERY,
2795
2862
  { handle: productHandle },
2796
2863
  { signal }
2797
2864
  );
@@ -2802,7 +2869,7 @@ var LimeBundleElement = class extends HTMLElement {
2802
2869
  const refs = data.product.metafield?.references?.nodes ?? [];
2803
2870
  const bundles = [];
2804
2871
  for (const ref of refs) {
2805
- const parsed = (0, import_core4.parseMetaobjectBundle)(ref.id, ref.fields);
2872
+ const parsed = (0, import_core5.parseMetaobjectBundle)(ref.id, ref.fields);
2806
2873
  if (parsed) bundles.push(parsed);
2807
2874
  }
2808
2875
  this.bundles = bundles;
@@ -2835,7 +2902,7 @@ var LimeBundleElement = class extends HTMLElement {
2835
2902
  */
2836
2903
  async defaultAddToCart(lines) {
2837
2904
  if (typeof window === "undefined") return;
2838
- const client = (0, import_core4.createStorefrontClient)({
2905
+ const client = (0, import_core5.createStorefrontClient)({
2839
2906
  shopDomain: this.shopDomain,
2840
2907
  accessToken: this.storefrontToken
2841
2908
  });
@@ -2846,7 +2913,7 @@ var LimeBundleElement = class extends HTMLElement {
2846
2913
  let checkoutUrl = null;
2847
2914
  if (existingCartId) {
2848
2915
  const res = await client.query(
2849
- import_core4.CART_LINES_ADD_MUTATION,
2916
+ import_core5.CART_LINES_ADD_MUTATION,
2850
2917
  { cartId: existingCartId, lines }
2851
2918
  );
2852
2919
  const payload = res.cartLinesAdd;
@@ -2858,7 +2925,7 @@ var LimeBundleElement = class extends HTMLElement {
2858
2925
  }
2859
2926
  if (!checkoutUrl) {
2860
2927
  const res = await client.query(
2861
- import_core4.CART_CREATE_MUTATION,
2928
+ import_core5.CART_CREATE_MUTATION,
2862
2929
  { input: { lines } }
2863
2930
  );
2864
2931
  const payload = res.cartCreate;
@@ -2904,7 +2971,7 @@ var LimeBundleElement = class extends HTMLElement {
2904
2971
  const price = variant ? parseFloat(variant.price.amount) : 0;
2905
2972
  return sum + price * line.quantity;
2906
2973
  }, 0);
2907
- (0, import_core4.reportAddToCart)(
2974
+ (0, import_core5.reportAddToCart)(
2908
2975
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
2909
2976
  {
2910
2977
  bundleGid: bundle.id,
@@ -2940,7 +3007,7 @@ var LimeBundleElement = class extends HTMLElement {
2940
3007
  container.setAttribute("aria-label", bundle.title);
2941
3008
  container.setAttribute("data-bundle-type", bundle.bundleType);
2942
3009
  container.setAttribute("data-bundle-gid", bundle.id);
2943
- (0, import_core5.applyWidgetConfigVars)(container, bundle.widgetConfig);
3010
+ (0, import_core6.applyWidgetConfigVars)(container, bundle.widgetConfig);
2944
3011
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
2945
3012
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
2946
3013
  switch (bundle.bundleType) {
@@ -2975,8 +3042,8 @@ var LimeBundleElement = class extends HTMLElement {
2975
3042
  }
2976
3043
  setupImpressionFor(bundle, element) {
2977
3044
  if (!this.analyticsEnabled || !this.appUrl) return;
2978
- const cleanup = (0, import_core4.observeImpression)(element, () => {
2979
- (0, import_core4.reportImpression)(
3045
+ const cleanup = (0, import_core5.observeImpression)(element, () => {
3046
+ (0, import_core5.reportImpression)(
2980
3047
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
2981
3048
  {
2982
3049
  bundleGid: bundle.id,