@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 +118 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +102 -33
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +82 -27
- package/dist/lime-bundle.global.js.map +1 -1
- package/docs/web-component.md +19 -0
- package/package.json +2 -2
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
|
|
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
|
|
416
|
-
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 =
|
|
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
|
|
633
|
+
setCtaLabel(cta, `Select ${requiredQty - count} more to unlock`);
|
|
619
634
|
} else {
|
|
620
635
|
cta.disabled = false;
|
|
621
|
-
cta
|
|
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
|
|
1357
|
-
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
|
|
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
|
-
/*
|
|
1476
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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,
|
|
2728
|
-
const sanitized = (0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
2979
|
-
(0,
|
|
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,
|