@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.js CHANGED
@@ -16,6 +16,11 @@ import {
16
16
  applyABVariantB
17
17
  } from "@lime-bundles/core";
18
18
 
19
+ // src/renderers/fixed.ts
20
+ import {
21
+ resolveBundleQty
22
+ } from "@lime-bundles/core";
23
+
19
24
  // src/renderers/pricing.ts
20
25
  import {
21
26
  parseCents,
@@ -70,6 +75,32 @@ function parseIso(iso) {
70
75
  return Number.isFinite(t) ? t : null;
71
76
  }
72
77
 
78
+ // src/renderers/cta-button.ts
79
+ function buildCtaButton(label) {
80
+ const button = document.createElement("button");
81
+ button.type = "button";
82
+ button.className = "lb-bundle-cta";
83
+ button.setAttribute("data-add-bundle", "");
84
+ const labelSpan = document.createElement("span");
85
+ labelSpan.className = "lb-cta-label";
86
+ labelSpan.setAttribute("data-cta-label", "");
87
+ labelSpan.textContent = label;
88
+ button.appendChild(labelSpan);
89
+ const spinnerSpan = document.createElement("span");
90
+ spinnerSpan.className = "lb-cta-spinner";
91
+ spinnerSpan.setAttribute("data-cta-spinner", "");
92
+ spinnerSpan.setAttribute("aria-hidden", "true");
93
+ 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>';
94
+ button.appendChild(spinnerSpan);
95
+ return button;
96
+ }
97
+ function setCtaLabel(button, text) {
98
+ const label = button.querySelector("[data-cta-label]");
99
+ if (label) {
100
+ label.textContent = text;
101
+ }
102
+ }
103
+
73
104
  // src/renderers/dom.ts
74
105
  function el(tag, className, attrs = {}) {
75
106
  const node = document.createElement(tag);
@@ -95,11 +126,7 @@ var PLACEHOLDER_THUMB_SVG = `
95
126
  </svg>`;
96
127
  function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
97
128
  const wc = bundle.widgetConfig;
98
- const qtyFor = (productId, variantId) => {
99
- const vq = bundle.variantQuantities[variantId];
100
- if (vq !== void 0) return vq;
101
- return bundle.productQuantities[productId] ?? 1;
102
- };
129
+ const qtyFor = (productId, variantId) => resolveBundleQty(bundle, productId, variantId);
103
130
  const rows = [];
104
131
  let oosCount = 0;
105
132
  bundle.products.forEach((product, idx) => {
@@ -190,9 +217,7 @@ function buildRowState(bundle, product, productIndex) {
190
217
  const eligibleVariants = selectedVariantIds && selectedVariantIds.length > 0 ? available.filter((v) => selectedVariantIds.includes(v.id)) : available;
191
218
  const isOos = eligibleVariants.length === 0;
192
219
  const selected = eligibleVariants[0] ?? null;
193
- const productQty = bundle.productQuantities[product.id] ?? 1;
194
- const variantQty = selected ? bundle.variantQuantities[selected.id] : void 0;
195
- const qty = variantQty ?? productQty;
220
+ const qty = selected ? resolveBundleQty(bundle, product.id, selected.id) : 1;
196
221
  return { product, eligibleVariants, selected, qty, isOos };
197
222
  }
198
223
  function renderHeader(bundle, currency) {
@@ -410,15 +435,11 @@ function renderSavingsBar() {
410
435
  };
411
436
  }
412
437
  function renderCta(bundle, oosCount, onClick) {
413
- const button = document.createElement("button");
414
- button.type = "button";
415
- button.className = "lb-bundle-cta";
416
- 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);
417
440
  if (oosCount > 0) {
418
441
  button.disabled = true;
419
- button.textContent = `${oosCount} item${oosCount === 1 ? "" : "s"} out of stock`;
420
442
  } else {
421
- button.textContent = bundle.widgetConfig.cta.ctaText || "Add to cart";
422
443
  button.addEventListener("click", () => {
423
444
  if (button.disabled) return;
424
445
  onClick();
@@ -515,12 +536,8 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
515
536
  isOverMax: () => selections.length >= maxQty
516
537
  });
517
538
  root.appendChild(modal.el);
518
- const cta = document.createElement("button");
519
- cta.type = "button";
520
- cta.className = "lb-bundle-cta";
521
- cta.setAttribute("data-add-bundle", "");
539
+ const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
522
540
  cta.disabled = true;
523
- cta.textContent = `Select ${requiredQty} items to unlock`;
524
541
  cta.addEventListener("click", () => {
525
542
  if (cta.disabled) return;
526
543
  const lines = selections.map((s) => ({
@@ -613,10 +630,10 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
613
630
  const count = selections.length;
614
631
  if (count < requiredQty) {
615
632
  cta.disabled = true;
616
- cta.textContent = `Select ${requiredQty - count} more to unlock`;
633
+ setCtaLabel(cta, `Select ${requiredQty - count} more to unlock`);
617
634
  } else {
618
635
  cta.disabled = false;
619
- cta.textContent = wc.cta.ctaText || "Add to cart";
636
+ setCtaLabel(cta, wc.cta.ctaText || "Add to cart");
620
637
  }
621
638
  }
622
639
  }
@@ -1351,12 +1368,9 @@ function renderSavingsBar3(resolved, selectedIndex, currency) {
1351
1368
  function renderCta2(bundle, onClick) {
1352
1369
  const product = bundle.products[0];
1353
1370
  const isAvailable = product?.variants.nodes.some((v) => v.availableForSale);
1354
- const button = document.createElement("button");
1355
- button.type = "button";
1356
- button.className = "lb-bundle-cta";
1357
- button.setAttribute("data-add-bundle", "");
1371
+ const label = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
1372
+ const button = buildCtaButton(label);
1358
1373
  if (!isAvailable) button.disabled = true;
1359
- button.textContent = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
1360
1374
  button.addEventListener("click", () => {
1361
1375
  if (button.disabled) return;
1362
1376
  onClick();
@@ -1470,11 +1484,20 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1470
1484
  display: none;
1471
1485
  }
1472
1486
 
1473
- /* Adjacent widget spacing \u2014 separates multiple bundles on the same product page */
1474
- .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 {
1475
1500
  margin-top: 24px;
1476
- padding-top: 24px;
1477
- border-top: 1px solid var(--lb-border);
1478
1501
  }
1479
1502
 
1480
1503
  /* Header \u2014 gradient banner with title + savings badge */
@@ -1743,9 +1766,16 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1743
1766
  display: none;
1744
1767
  }
1745
1768
 
1746
- /* 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. */
1747
1775
  .lb-bundle-cta {
1748
- display: block;
1776
+ display: grid;
1777
+ grid-template-rows: 1fr;
1778
+ grid-template-columns: 1fr;
1749
1779
  width: 100%;
1750
1780
  padding: 16px;
1751
1781
  border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);
@@ -1774,12 +1804,51 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1774
1804
  cursor: not-allowed;
1775
1805
  }
1776
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
+
1777
1833
  .lb-bundle-cta[data-loading="true"] {
1778
- opacity: 0.7;
1779
1834
  cursor: wait;
1780
1835
  pointer-events: none;
1781
1836
  }
1782
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
+
1783
1852
  /* Error message */
1784
1853
  .lb-bundle-error {
1785
1854
  font-size: 16px;