@lime-bundles/widget 2.5.0 → 2.6.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 +160 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -37
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +82 -40
- package/dist/lime-bundle.global.js.map +1 -1
- package/docs/css-variables.md +11 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
|
|
19
19
|
// src/renderers/fixed.ts
|
|
20
20
|
import {
|
|
21
|
-
resolveBundleQty
|
|
21
|
+
resolveBundleQty,
|
|
22
|
+
isVariantFulfillable,
|
|
23
|
+
shouldShowLowStockBadge
|
|
22
24
|
} from "@lime-bundles/core";
|
|
23
25
|
|
|
24
26
|
// src/dropdown/bind-dropdown.ts
|
|
@@ -534,9 +536,18 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
534
536
|
const list = el("div", "lb-fixed__products");
|
|
535
537
|
const rowHandles = [];
|
|
536
538
|
rows.forEach((rowState) => {
|
|
537
|
-
const handle = renderProductRow(
|
|
538
|
-
|
|
539
|
-
|
|
539
|
+
const handle = renderProductRow(
|
|
540
|
+
rowState,
|
|
541
|
+
currency,
|
|
542
|
+
qtyFor,
|
|
543
|
+
{
|
|
544
|
+
enabled: wc.showLowStockBadge,
|
|
545
|
+
threshold: wc.lowStockThreshold
|
|
546
|
+
},
|
|
547
|
+
() => {
|
|
548
|
+
updatePricing();
|
|
549
|
+
}
|
|
550
|
+
);
|
|
540
551
|
rowHandles.push(handle);
|
|
541
552
|
list.appendChild(handle.el);
|
|
542
553
|
});
|
|
@@ -592,7 +603,9 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
592
603
|
function buildRowState(bundle, product, productIndex) {
|
|
593
604
|
const selectedVariantIds = bundle.selectedVariantIds?.[productIndex] ?? null;
|
|
594
605
|
const merchantScoped = selectedVariantIds && selectedVariantIds.length > 0 ? product.variants.nodes.filter((v) => selectedVariantIds.includes(v.id)) : product.variants.nodes;
|
|
595
|
-
const firstInStock = merchantScoped.find(
|
|
606
|
+
const firstInStock = merchantScoped.find(
|
|
607
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
|
|
608
|
+
) ?? null;
|
|
596
609
|
const eligibleVariants = merchantScoped;
|
|
597
610
|
const isOos = !firstInStock;
|
|
598
611
|
const selected = firstInStock ?? merchantScoped[0] ?? null;
|
|
@@ -656,7 +669,7 @@ function deriveHeaderBadge(bundle, totalCents, saleCents, currency) {
|
|
|
656
669
|
}
|
|
657
670
|
return `-${formatCents(savings, currency)}`;
|
|
658
671
|
}
|
|
659
|
-
function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
672
|
+
function renderProductRow(state, currency, qtyFor, lowStock, onVariantChange) {
|
|
660
673
|
const rowEl = el(
|
|
661
674
|
"div",
|
|
662
675
|
state.isOos ? "lb-bundle-product-row lb-bundle-product-row--oos" : "lb-bundle-product-row",
|
|
@@ -703,6 +716,12 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
|
703
716
|
oosLabel.textContent = "Out of stock";
|
|
704
717
|
info.appendChild(oosLabel);
|
|
705
718
|
} else if (state.selected) {
|
|
719
|
+
const isSinglePinnedVariant = state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1;
|
|
720
|
+
if (isSinglePinnedVariant) {
|
|
721
|
+
const badge = el("span", "lb-bundle-variant-badge");
|
|
722
|
+
badge.textContent = state.eligibleVariants[0].title;
|
|
723
|
+
info.appendChild(badge);
|
|
724
|
+
}
|
|
706
725
|
const prices = el("span", "lb-bundle-product-prices");
|
|
707
726
|
const compare = el("span", "lb-bundle-product-compare-price", {
|
|
708
727
|
"data-product-compare-price": ""
|
|
@@ -718,6 +737,11 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
|
718
737
|
});
|
|
719
738
|
unitPriceEl.setAttribute("hidden", "");
|
|
720
739
|
info.appendChild(unitPriceEl);
|
|
740
|
+
const lowStockEl = el("span", "lb-bundle-low-stock-badge", {
|
|
741
|
+
"data-low-stock-badge": ""
|
|
742
|
+
});
|
|
743
|
+
lowStockEl.setAttribute("hidden", "");
|
|
744
|
+
info.appendChild(lowStockEl);
|
|
721
745
|
const applyVariantToRow = (variant) => {
|
|
722
746
|
const unit = parseCents(variant.price.amount);
|
|
723
747
|
priceEl.textContent = formatCents(unit, currency);
|
|
@@ -752,6 +776,18 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
|
752
776
|
});
|
|
753
777
|
thumbImg.alt = nextImage.altText ?? state.product.title;
|
|
754
778
|
}
|
|
779
|
+
const required = qtyFor(state.product.id, variant.id);
|
|
780
|
+
if (shouldShowLowStockBadge(
|
|
781
|
+
variant,
|
|
782
|
+
required,
|
|
783
|
+
lowStock.threshold,
|
|
784
|
+
lowStock.enabled
|
|
785
|
+
)) {
|
|
786
|
+
lowStockEl.textContent = `Only ${variant.quantityAvailable} left`;
|
|
787
|
+
lowStockEl.removeAttribute("hidden");
|
|
788
|
+
} else {
|
|
789
|
+
lowStockEl.setAttribute("hidden", "");
|
|
790
|
+
}
|
|
755
791
|
};
|
|
756
792
|
applyVariantToRow(state.selected);
|
|
757
793
|
if (state.eligibleVariants.length > 1) {
|
|
@@ -770,7 +806,7 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
|
770
806
|
});
|
|
771
807
|
};
|
|
772
808
|
const isValueAvailable = (optionIndex, value, selected) => state.eligibleVariants.some((v) => {
|
|
773
|
-
if (!v.
|
|
809
|
+
if (!isVariantFulfillable(v, qtyFor(state.product.id, v.id))) return false;
|
|
774
810
|
if (v.selectedOptions[optionIndex]?.value !== value) return false;
|
|
775
811
|
return v.selectedOptions.every(
|
|
776
812
|
(o, i) => i === optionIndex || o.value === selected[i]
|
|
@@ -834,10 +870,6 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
|
834
870
|
if (state.selected) {
|
|
835
871
|
recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
|
|
836
872
|
}
|
|
837
|
-
} else if (state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1) {
|
|
838
|
-
const badge = el("span", "lb-bundle-variant-badge");
|
|
839
|
-
badge.textContent = state.eligibleVariants[0].title;
|
|
840
|
-
info.appendChild(badge);
|
|
841
873
|
}
|
|
842
874
|
}
|
|
843
875
|
rowEl.appendChild(info);
|
|
@@ -922,7 +954,7 @@ function computeSale(totalCents, discount, rows) {
|
|
|
922
954
|
}
|
|
923
955
|
|
|
924
956
|
// src/renderers/mix-match.ts
|
|
925
|
-
import { resolveBundleQty as resolveBundleQty2 } from "@lime-bundles/core";
|
|
957
|
+
import { resolveBundleQty as resolveBundleQty2, isVariantFulfillable as isVariantFulfillable2, shouldShowLowStockBadge as shouldShowLowStockBadge2 } from "@lime-bundles/core";
|
|
926
958
|
var PLACEHOLDER_THUMB_SVG2 = `
|
|
927
959
|
<svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
928
960
|
<rect x="4" y="4" width="20" height="20" rx="3"></rect>
|
|
@@ -1412,7 +1444,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1412
1444
|
list.innerHTML = "";
|
|
1413
1445
|
eligible.forEach((ep) => {
|
|
1414
1446
|
const availableVariants = ep.variants;
|
|
1415
|
-
const firstAvailVariant = ep.variants.find(
|
|
1447
|
+
const firstAvailVariant = ep.variants.find(
|
|
1448
|
+
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, ep.product.id, v.id))
|
|
1449
|
+
) ?? ep.firstAvailableVariant ?? ep.variants[0];
|
|
1416
1450
|
if (!firstAvailVariant) return;
|
|
1417
1451
|
let currentVariant = firstAvailVariant;
|
|
1418
1452
|
const productEl = el(
|
|
@@ -1421,7 +1455,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1421
1455
|
{ "data-product-id": ep.product.id.replace(/^.*\//, "") }
|
|
1422
1456
|
);
|
|
1423
1457
|
const thumb = el("div", "lb-mix-match__modal-product-thumb");
|
|
1424
|
-
const initialThumbVariant = ep.variants.find(
|
|
1458
|
+
const initialThumbVariant = ep.variants.find(
|
|
1459
|
+
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, ep.product.id, v.id))
|
|
1460
|
+
) ?? ep.variants[0] ?? null;
|
|
1425
1461
|
const initialThumbImage = initialThumbVariant?.image ?? ep.product.featuredImage ?? null;
|
|
1426
1462
|
let thumbImg = null;
|
|
1427
1463
|
if (initialThumbImage) {
|
|
@@ -1470,6 +1506,26 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1470
1506
|
unitPrice.hidden = true;
|
|
1471
1507
|
}
|
|
1472
1508
|
info.appendChild(unitPrice);
|
|
1509
|
+
const lowStockBadge = el("span", "lb-bundle-low-stock-badge", {
|
|
1510
|
+
"data-low-stock-badge": ""
|
|
1511
|
+
});
|
|
1512
|
+
lowStockBadge.hidden = true;
|
|
1513
|
+
info.appendChild(lowStockBadge);
|
|
1514
|
+
const refreshLowStockBadge = (variant) => {
|
|
1515
|
+
const required = resolveBundleQty2(bundle, ep.product.id, variant.id);
|
|
1516
|
+
if (shouldShowLowStockBadge2(
|
|
1517
|
+
variant,
|
|
1518
|
+
required,
|
|
1519
|
+
bundle.widgetConfig.lowStockThreshold,
|
|
1520
|
+
bundle.widgetConfig.showLowStockBadge
|
|
1521
|
+
)) {
|
|
1522
|
+
lowStockBadge.textContent = `Only ${variant.quantityAvailable} left`;
|
|
1523
|
+
lowStockBadge.hidden = false;
|
|
1524
|
+
} else {
|
|
1525
|
+
lowStockBadge.hidden = true;
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
refreshLowStockBadge(currentVariant);
|
|
1473
1529
|
const row = {
|
|
1474
1530
|
el: productEl,
|
|
1475
1531
|
product: ep.product,
|
|
@@ -1493,7 +1549,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1493
1549
|
});
|
|
1494
1550
|
};
|
|
1495
1551
|
const isValueAvailable = (optionIndex, value, selected) => availableVariants.some((v) => {
|
|
1496
|
-
if (!v.
|
|
1552
|
+
if (!isVariantFulfillable2(v, resolveBundleQty2(bundle, ep.product.id, v.id))) return false;
|
|
1497
1553
|
if (v.selectedOptions[optionIndex]?.value !== value) return false;
|
|
1498
1554
|
return v.selectedOptions.every(
|
|
1499
1555
|
(o, i) => i === optionIndex || o.value === selected[i]
|
|
@@ -1549,6 +1605,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1549
1605
|
thumbImg.alt = nextImage.altText ?? ep.product.title;
|
|
1550
1606
|
}
|
|
1551
1607
|
recomputeDisabled(next.selectedOptions.map((o) => o.value));
|
|
1608
|
+
refreshLowStockBadge(currentVariant);
|
|
1552
1609
|
rowUpdateCount();
|
|
1553
1610
|
};
|
|
1554
1611
|
const groupsContainer = el("div", "lb-bundle-variant-option-groups");
|
|
@@ -1684,7 +1741,9 @@ function buildEligibleProducts(bundle, oosBehavior) {
|
|
|
1684
1741
|
for (const product of bundle.products) {
|
|
1685
1742
|
if (seen.has(product.id)) continue;
|
|
1686
1743
|
seen.add(product.id);
|
|
1687
|
-
const available = product.variants.nodes.filter(
|
|
1744
|
+
const available = product.variants.nodes.filter(
|
|
1745
|
+
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, product.id, v.id))
|
|
1746
|
+
);
|
|
1688
1747
|
const isOos = available.length === 0;
|
|
1689
1748
|
if (isOos && oosBehavior === "hide") continue;
|
|
1690
1749
|
result.push({
|
|
@@ -1717,10 +1776,14 @@ function sanitizeId(gid) {
|
|
|
1717
1776
|
}
|
|
1718
1777
|
|
|
1719
1778
|
// src/renderers/volume.ts
|
|
1779
|
+
import { isVariantFulfillable as isVariantFulfillable3, shouldShowLowStockBadge as shouldShowLowStockBadge3 } from "@lime-bundles/core";
|
|
1720
1780
|
function renderVolumeBundle(container, bundle, onAddToCart, onCleanup) {
|
|
1721
1781
|
const wc = bundle.widgetConfig;
|
|
1722
1782
|
const product = bundle.products[0];
|
|
1723
|
-
const
|
|
1783
|
+
const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;
|
|
1784
|
+
const variant = product?.variants.nodes.find(
|
|
1785
|
+
(v) => isVariantFulfillable3(v, minTierQty)
|
|
1786
|
+
);
|
|
1724
1787
|
if (!variant && wc.outOfStockBehavior === "hide") return;
|
|
1725
1788
|
const basePriceCents = variant ? parseCents(variant.price.amount) : 0;
|
|
1726
1789
|
const currency = variant?.price.currencyCode ?? "USD";
|
|
@@ -1819,6 +1882,18 @@ function renderVolumeBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1819
1882
|
}
|
|
1820
1883
|
]);
|
|
1821
1884
|
});
|
|
1885
|
+
if (variant && shouldShowLowStockBadge3(
|
|
1886
|
+
variant,
|
|
1887
|
+
minTierQty,
|
|
1888
|
+
wc.lowStockThreshold,
|
|
1889
|
+
wc.showLowStockBadge
|
|
1890
|
+
)) {
|
|
1891
|
+
const lowStock = el("span", "lb-bundle-low-stock-badge", {
|
|
1892
|
+
"data-low-stock-badge": ""
|
|
1893
|
+
});
|
|
1894
|
+
lowStock.textContent = `Only ${variant.quantityAvailable} left`;
|
|
1895
|
+
root.appendChild(lowStock);
|
|
1896
|
+
}
|
|
1822
1897
|
root.appendChild(cta);
|
|
1823
1898
|
root.appendChild(
|
|
1824
1899
|
el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
|
|
@@ -1972,7 +2047,10 @@ function renderSavingsBar3(resolved, selectedIndex, currency) {
|
|
|
1972
2047
|
}
|
|
1973
2048
|
function renderCta2(bundle, onClick) {
|
|
1974
2049
|
const product = bundle.products[0];
|
|
1975
|
-
const
|
|
2050
|
+
const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;
|
|
2051
|
+
const isAvailable = product?.variants.nodes.some(
|
|
2052
|
+
(v) => isVariantFulfillable3(v, minTierQty)
|
|
2053
|
+
);
|
|
1976
2054
|
const label = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
|
|
1977
2055
|
const button = buildCtaButton(label);
|
|
1978
2056
|
if (!isAvailable) button.disabled = true;
|
|
@@ -2349,15 +2427,19 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
|
|
|
2349
2427
|
display: none;
|
|
2350
2428
|
}
|
|
2351
2429
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2430
|
+
/* Read-only variant text shown below the product title when only one
|
|
2431
|
+
variant is in scope (single-variant product, or merchant pinned a
|
|
2432
|
+
single variant). Same visual treatment as the mix-match filled
|
|
2433
|
+
slot's variant text \u2014 see .lb-mix-match__filled-variant in
|
|
2434
|
+
bundle-mix-match.css. Both share this rule via the comma selector
|
|
2435
|
+
so the storefront UX stays consistent across bundle types. */
|
|
2436
|
+
.lb-bundle-variant-badge,
|
|
2437
|
+
.lb-mix-match__slot--filled .lb-mix-match__filled-variant {
|
|
2438
|
+
display: block;
|
|
2357
2439
|
font-size: 12px;
|
|
2358
|
-
line-height:
|
|
2440
|
+
line-height: 20px;
|
|
2441
|
+
margin-top: 2px;
|
|
2359
2442
|
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
2360
|
-
margin-top: 8px;
|
|
2361
2443
|
}
|
|
2362
2444
|
|
|
2363
2445
|
/* Per-option variant pickers. Each option's label + select sit inside a
|
|
@@ -2401,10 +2483,10 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
|
|
|
2401
2483
|
click-to-focus doesn't leave a keyboard-style ring. The modal overlay
|
|
2402
2484
|
gets its own selector because the Liquid path reparents it to body,
|
|
2403
2485
|
outside the widget root. */
|
|
2404
|
-
.using-mouse .lb-bundle-widget :focus,
|
|
2405
|
-
.using-mouse .lb-bundle-widget :focus-visible,
|
|
2406
|
-
.using-mouse .lb-mix-match__modal-overlay :focus,
|
|
2407
|
-
.using-mouse .lb-mix-match__modal-overlay :focus-visible {
|
|
2486
|
+
.using-mouse .lb-bundle-widget :focus:not([aria-checked="true"]),
|
|
2487
|
+
.using-mouse .lb-bundle-widget :focus-visible:not([aria-checked="true"]),
|
|
2488
|
+
.using-mouse .lb-mix-match__modal-overlay :focus:not([aria-checked="true"]),
|
|
2489
|
+
.using-mouse .lb-mix-match__modal-overlay :focus-visible:not([aria-checked="true"]) {
|
|
2408
2490
|
outline: none;
|
|
2409
2491
|
outline-offset: 0;
|
|
2410
2492
|
box-shadow: none;
|
|
@@ -2631,6 +2713,24 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
|
|
|
2631
2713
|
margin-left: auto;
|
|
2632
2714
|
}
|
|
2633
2715
|
|
|
2716
|
+
/* "Only X left" low-stock badge \u2014 appears alongside product/variant
|
|
2717
|
+
info when ProductVariant.quantityAvailable falls at or below
|
|
2718
|
+
wc.lowStockThreshold. Hidden when quantityAvailable is unknown
|
|
2719
|
+
(Storefront token without read_product_inventory). */
|
|
2720
|
+
.lb-bundle-low-stock-badge {
|
|
2721
|
+
display: inline-flex;
|
|
2722
|
+
align-items: center;
|
|
2723
|
+
gap: 4px;
|
|
2724
|
+
padding: 2px 8px;
|
|
2725
|
+
font-size: 11px;
|
|
2726
|
+
font-weight: 600;
|
|
2727
|
+
line-height: 1.4;
|
|
2728
|
+
color: var(--lb-low-stock-text);
|
|
2729
|
+
background-color: var(--lb-low-stock-bg);
|
|
2730
|
+
border-radius: 4px;
|
|
2731
|
+
white-space: nowrap;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2634
2734
|
/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */
|
|
2635
2735
|
.lb-ab-pending {
|
|
2636
2736
|
visibility: hidden;
|
|
@@ -2854,12 +2954,9 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
2854
2954
|
text-decoration: underline;
|
|
2855
2955
|
}
|
|
2856
2956
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
margin-top: 2px;
|
|
2861
|
-
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
2862
|
-
}
|
|
2957
|
+
/* .lb-mix-match__filled-variant \u2014 styled jointly with
|
|
2958
|
+
.lb-bundle-variant-badge above to keep the variant text consistent
|
|
2959
|
+
across mix-match and fixed bundle widgets. */
|
|
2863
2960
|
|
|
2864
2961
|
.lb-mix-match__filled-price {
|
|
2865
2962
|
/* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
@@ -3283,10 +3380,21 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
|
|
|
3283
3380
|
}
|
|
3284
3381
|
|
|
3285
3382
|
.lb-volume__tier:hover {
|
|
3286
|
-
border-color:
|
|
3383
|
+
border-color: var(--lb-tier-selected-border-color);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
/* Suppress the UA default focus outline so a freshly-clicked selected
|
|
3387
|
+
tier doesn't briefly show the 1px focus ring on top of (or in place of)
|
|
3388
|
+
the custom selected-state outline below. Keyboard focus is still
|
|
3389
|
+
indicated by the :focus-visible rule. */
|
|
3390
|
+
.lb-volume__tier:focus {
|
|
3391
|
+
outline: none;
|
|
3287
3392
|
}
|
|
3288
3393
|
|
|
3289
|
-
|
|
3394
|
+
/* Keyboard focus indicator \u2014 explicitly excludes the selected tier so
|
|
3395
|
+
the selected-state outline rule below has full ownership of the
|
|
3396
|
+
outline property when both states apply at once. */
|
|
3397
|
+
.lb-volume__tier:focus-visible:not([aria-checked="true"]) {
|
|
3290
3398
|
outline: 2px solid var(--lb-primary-color);
|
|
3291
3399
|
outline-offset: 2px;
|
|
3292
3400
|
}
|