@lime-bundles/widget 1.0.0 → 2.1.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/README.md +76 -72
- package/dist/index.cjs +2643 -344
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -50
- package/dist/index.d.ts +22 -50
- package/dist/index.js +2654 -352
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +1243 -86
- package/dist/lime-bundle.global.js.map +1 -1
- package/docs/README.md +81 -0
- package/docs/css-variables.md +158 -0
- package/docs/hydrogen.md +185 -0
- package/docs/react-nextjs.md +379 -0
- package/docs/web-component.md +282 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -11,369 +11,2579 @@ import {
|
|
|
11
11
|
reportImpression,
|
|
12
12
|
reportAddToCart,
|
|
13
13
|
injectCustomCss,
|
|
14
|
-
|
|
14
|
+
sanitizeCustomCss,
|
|
15
|
+
getABTestAssignment,
|
|
16
|
+
applyABVariantB
|
|
15
17
|
} from "@lime-bundles/core";
|
|
16
18
|
|
|
17
|
-
// src/renderers/
|
|
19
|
+
// src/renderers/pricing.ts
|
|
20
|
+
import {
|
|
21
|
+
parseCents,
|
|
22
|
+
formatCents,
|
|
23
|
+
percentageDiscountUnit,
|
|
24
|
+
computeFixedPricing,
|
|
25
|
+
computeBundleSaleCents
|
|
26
|
+
} from "@lime-bundles/core";
|
|
27
|
+
|
|
28
|
+
// src/renderers/countdown.ts
|
|
29
|
+
import { formatCountdown } from "@lime-bundles/core";
|
|
30
|
+
function renderCountdown(endsAtIso) {
|
|
31
|
+
const parsed = parseIso(endsAtIso);
|
|
32
|
+
if (parsed === null) return null;
|
|
33
|
+
if (parsed <= Date.now()) return null;
|
|
34
|
+
const target = parsed;
|
|
35
|
+
const wrap = document.createElement("div");
|
|
36
|
+
wrap.className = "lb-bundle-countdown";
|
|
37
|
+
wrap.setAttribute("data-countdown", "");
|
|
38
|
+
const labelWrap = document.createElement("div");
|
|
39
|
+
labelWrap.className = "lb-bundle-countdown__label";
|
|
40
|
+
const labelText = document.createElement("span");
|
|
41
|
+
labelText.textContent = "Ends in";
|
|
42
|
+
labelWrap.appendChild(labelText);
|
|
43
|
+
wrap.appendChild(labelWrap);
|
|
44
|
+
const timer = document.createElement("span");
|
|
45
|
+
timer.className = "lb-bundle-countdown__timer";
|
|
46
|
+
timer.setAttribute("data-countdown-timer", "");
|
|
47
|
+
wrap.appendChild(timer);
|
|
48
|
+
let intervalId = null;
|
|
49
|
+
function tick() {
|
|
50
|
+
const msLeft = target - Date.now();
|
|
51
|
+
if (msLeft <= 0) {
|
|
52
|
+
wrap.style.display = "none";
|
|
53
|
+
stop();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
timer.textContent = formatCountdown(msLeft);
|
|
57
|
+
}
|
|
58
|
+
function stop() {
|
|
59
|
+
if (intervalId !== null) {
|
|
60
|
+
clearInterval(intervalId);
|
|
61
|
+
intervalId = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
tick();
|
|
65
|
+
intervalId = setInterval(tick, 1e3);
|
|
66
|
+
return { el: wrap, stop };
|
|
67
|
+
}
|
|
68
|
+
function parseIso(iso) {
|
|
69
|
+
const t = Date.parse(iso);
|
|
70
|
+
return Number.isFinite(t) ? t : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/renderers/dom.ts
|
|
74
|
+
function el(tag, className, attrs = {}) {
|
|
75
|
+
const node = document.createElement(tag);
|
|
76
|
+
if (className) node.className = className;
|
|
77
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
78
|
+
node.setAttribute(k, v);
|
|
79
|
+
}
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/renderers/image.ts
|
|
18
84
|
import {
|
|
19
|
-
|
|
85
|
+
transformImageUrl,
|
|
86
|
+
THUMB_PX
|
|
20
87
|
} from "@lime-bundles/core";
|
|
21
|
-
|
|
88
|
+
|
|
89
|
+
// src/renderers/fixed.ts
|
|
90
|
+
var PLACEHOLDER_THUMB_SVG = `
|
|
91
|
+
<svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
92
|
+
<rect x="4" y="4" width="20" height="20" rx="3"></rect>
|
|
93
|
+
<line x1="4" y1="20" x2="24" y2="20"></line>
|
|
94
|
+
<circle cx="10" cy="12" r="2"></circle>
|
|
95
|
+
</svg>`;
|
|
96
|
+
function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
97
|
+
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
|
+
};
|
|
103
|
+
const rows = [];
|
|
104
|
+
let oosCount = 0;
|
|
105
|
+
bundle.products.forEach((product, idx) => {
|
|
106
|
+
const row = buildRowState(bundle, product, idx);
|
|
107
|
+
if (row.qty === 0) return;
|
|
108
|
+
if (row.isOos) {
|
|
109
|
+
oosCount++;
|
|
110
|
+
if (wc.outOfStockBehavior === "hide") return;
|
|
111
|
+
}
|
|
112
|
+
rows.push(row);
|
|
113
|
+
});
|
|
114
|
+
if (rows.length === 0) return;
|
|
22
115
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
23
|
-
const
|
|
24
|
-
|
|
116
|
+
const root = el("div", "lb-fixed", {
|
|
117
|
+
"data-discount-type": bundle.discountConfig.discountType,
|
|
118
|
+
"data-discount-value": String(bundle.discountConfig.discountValue)
|
|
119
|
+
});
|
|
120
|
+
const headerHandle = renderHeader(bundle, currency);
|
|
121
|
+
root.appendChild(headerHandle.el);
|
|
122
|
+
if (wc.countdown.showCountdown && bundle.endsAt) {
|
|
123
|
+
const countdown = renderCountdown(bundle.endsAt);
|
|
124
|
+
if (countdown) {
|
|
125
|
+
root.appendChild(countdown.el);
|
|
126
|
+
onCleanup?.(countdown.stop);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const list = el("div", "lb-fixed__products");
|
|
130
|
+
const rowHandles = [];
|
|
131
|
+
rows.forEach((rowState) => {
|
|
132
|
+
const handle = renderProductRow(rowState, currency, qtyFor, () => {
|
|
133
|
+
updatePricing();
|
|
134
|
+
});
|
|
135
|
+
rowHandles.push(handle);
|
|
136
|
+
list.appendChild(handle.el);
|
|
137
|
+
});
|
|
138
|
+
root.appendChild(list);
|
|
139
|
+
root.appendChild(el("div", "lb-bundle-divider"));
|
|
140
|
+
const pricingHandle = renderPricingRow(bundle);
|
|
141
|
+
root.appendChild(pricingHandle.el);
|
|
142
|
+
const savingsBarHandle = wc.savingsBar.visible ? renderSavingsBar() : null;
|
|
143
|
+
if (savingsBarHandle) root.appendChild(savingsBarHandle.el);
|
|
144
|
+
const cta = renderCta(bundle, oosCount, () => {
|
|
145
|
+
const lines = rows.filter((r) => r.selected).map((r) => ({
|
|
146
|
+
merchandiseId: r.selected.id,
|
|
147
|
+
quantity: r.qty,
|
|
148
|
+
attributes: [
|
|
149
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
150
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
151
|
+
]
|
|
152
|
+
}));
|
|
153
|
+
if (lines.length === 0) return;
|
|
154
|
+
onAddToCart(lines);
|
|
155
|
+
});
|
|
156
|
+
root.appendChild(cta);
|
|
157
|
+
root.appendChild(
|
|
158
|
+
el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
|
|
159
|
+
);
|
|
160
|
+
root.appendChild(
|
|
161
|
+
el("span", "lb-visually-hidden", {
|
|
162
|
+
"data-status": "",
|
|
163
|
+
"aria-live": "polite"
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
container.appendChild(root);
|
|
167
|
+
updatePricing();
|
|
168
|
+
function updatePricing() {
|
|
169
|
+
const totalCents = rows.reduce((sum, r) => {
|
|
170
|
+
if (!r.selected) return sum;
|
|
171
|
+
const unit = parseCents(r.selected.price.amount);
|
|
172
|
+
return sum + unit * r.qty;
|
|
173
|
+
}, 0);
|
|
174
|
+
const saleCents = computeSale(totalCents, bundle.discountConfig, rows);
|
|
175
|
+
const savingsCents = Math.max(0, totalCents - saleCents);
|
|
176
|
+
pricingHandle.update({ totalCents, saleCents, savingsCents, currency });
|
|
177
|
+
if (savingsBarHandle) {
|
|
178
|
+
savingsBarHandle.update({ savingsCents, currency });
|
|
179
|
+
}
|
|
180
|
+
headerHandle.refresh(
|
|
181
|
+
deriveHeaderBadge(bundle, totalCents, saleCents, currency)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function buildRowState(bundle, product, productIndex) {
|
|
186
|
+
const selectedVariantIds = bundle.selectedVariantIds?.[productIndex] ?? null;
|
|
187
|
+
const available = product.variants.nodes.filter(
|
|
188
|
+
(v) => v.availableForSale
|
|
189
|
+
);
|
|
190
|
+
const eligibleVariants = selectedVariantIds && selectedVariantIds.length > 0 ? available.filter((v) => selectedVariantIds.includes(v.id)) : available;
|
|
191
|
+
const isOos = eligibleVariants.length === 0;
|
|
192
|
+
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;
|
|
196
|
+
return { product, eligibleVariants, selected, qty, isOos };
|
|
197
|
+
}
|
|
198
|
+
function renderHeader(bundle, currency) {
|
|
199
|
+
const wc = bundle.widgetConfig;
|
|
200
|
+
const header = el("div", "lb-bundle-header");
|
|
201
|
+
const content = el("div", "lb-bundle-header__content");
|
|
202
|
+
const title = el("h3", "lb-bundle-title");
|
|
25
203
|
title.textContent = bundle.title;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
badge.textContent = bundle.discountLabel;
|
|
32
|
-
container.appendChild(badge);
|
|
33
|
-
}
|
|
34
|
-
const productsDiv = document.createElement("div");
|
|
35
|
-
productsDiv.className = "lb-bundle__products";
|
|
36
|
-
for (const product of bundle.products) {
|
|
37
|
-
const productEl = document.createElement("div");
|
|
38
|
-
productEl.className = "lb-bundle__product";
|
|
39
|
-
productEl.setAttribute("part", "product");
|
|
40
|
-
if (product.featuredImage) {
|
|
41
|
-
const img = document.createElement("img");
|
|
42
|
-
img.src = product.featuredImage.url;
|
|
43
|
-
img.alt = product.featuredImage.altText ?? product.title;
|
|
44
|
-
img.className = "lb-bundle__product-image";
|
|
45
|
-
img.loading = "lazy";
|
|
46
|
-
productEl.appendChild(img);
|
|
47
|
-
}
|
|
48
|
-
const info = document.createElement("div");
|
|
49
|
-
info.className = "lb-bundle__product-info";
|
|
50
|
-
info.innerHTML = `
|
|
51
|
-
<p class="lb-bundle__product-title">${escapeHtml(product.title)}</p>
|
|
52
|
-
<p class="lb-bundle__product-price">${escapeHtml(formatMoney(product.priceRange.minVariantPrice.amount, currency))}</p>
|
|
53
|
-
`;
|
|
54
|
-
productEl.appendChild(info);
|
|
55
|
-
productsDiv.appendChild(productEl);
|
|
204
|
+
content.appendChild(title);
|
|
205
|
+
if (bundle.description) {
|
|
206
|
+
const subtitle = el("p", "lb-bundle-subtitle");
|
|
207
|
+
subtitle.textContent = bundle.description;
|
|
208
|
+
content.appendChild(subtitle);
|
|
56
209
|
}
|
|
57
|
-
|
|
210
|
+
header.appendChild(content);
|
|
211
|
+
const badgeEl = el("span", "lb-bundle-header__badge", {
|
|
212
|
+
"data-header-badge": ""
|
|
213
|
+
});
|
|
214
|
+
header.appendChild(badgeEl);
|
|
215
|
+
const initialPricing = computeFixedPricing(
|
|
216
|
+
bundle,
|
|
217
|
+
bundle.productQuantities,
|
|
218
|
+
wc.pricing.showSaveBadge
|
|
219
|
+
);
|
|
220
|
+
if (initialPricing.headerBadge) {
|
|
221
|
+
badgeEl.textContent = initialPricing.headerBadge;
|
|
222
|
+
} else {
|
|
223
|
+
badgeEl.style.display = "none";
|
|
224
|
+
}
|
|
225
|
+
void currency;
|
|
226
|
+
return {
|
|
227
|
+
el: header,
|
|
228
|
+
refresh(badgeText) {
|
|
229
|
+
if (!wc.pricing.showSaveBadge) {
|
|
230
|
+
badgeEl.style.display = "none";
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (badgeText) {
|
|
234
|
+
badgeEl.textContent = badgeText;
|
|
235
|
+
badgeEl.style.display = "";
|
|
236
|
+
} else {
|
|
237
|
+
badgeEl.style.display = "none";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function deriveHeaderBadge(bundle, totalCents, saleCents, currency) {
|
|
243
|
+
if (!bundle.widgetConfig.pricing.showSaveBadge) return "";
|
|
244
|
+
const savings = totalCents - saleCents;
|
|
245
|
+
if (savings <= 0) return "";
|
|
246
|
+
const dc = bundle.discountConfig;
|
|
247
|
+
if (dc.discountType === "percentage" && dc.discountValue > 0) {
|
|
248
|
+
return `-${Math.round(dc.discountValue)}%`;
|
|
249
|
+
}
|
|
250
|
+
if (dc.discountType === "fixed_amount" && dc.discountValue > 0) {
|
|
251
|
+
return `-${formatCents(Math.round(dc.discountValue * 100), currency)}`;
|
|
252
|
+
}
|
|
253
|
+
return `-${formatCents(savings, currency)}`;
|
|
254
|
+
}
|
|
255
|
+
function renderProductRow(state, currency, qtyFor, onVariantChange) {
|
|
256
|
+
const rowEl = el(
|
|
257
|
+
"div",
|
|
258
|
+
state.isOos ? "lb-bundle-product-row lb-bundle-product-row--oos" : "lb-bundle-product-row",
|
|
259
|
+
{
|
|
260
|
+
"data-product-id": state.product.id.replace(/^.*\//, ""),
|
|
261
|
+
...state.isOos ? { "aria-disabled": "true" } : {}
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
const thumb = el("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
|
|
265
|
+
if (state.product.featuredImage) {
|
|
266
|
+
const img = document.createElement("img");
|
|
267
|
+
img.src = transformImageUrl(state.product.featuredImage.url, {
|
|
268
|
+
width: THUMB_PX,
|
|
269
|
+
height: THUMB_PX,
|
|
270
|
+
crop: "center"
|
|
271
|
+
});
|
|
272
|
+
img.alt = state.product.featuredImage.altText ?? state.product.title;
|
|
273
|
+
img.width = THUMB_PX;
|
|
274
|
+
img.height = THUMB_PX;
|
|
275
|
+
img.loading = "lazy";
|
|
276
|
+
thumb.appendChild(img);
|
|
277
|
+
} else {
|
|
278
|
+
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG);
|
|
279
|
+
}
|
|
280
|
+
let qtyBadgeRef = null;
|
|
281
|
+
if (!state.isOos) {
|
|
282
|
+
qtyBadgeRef = el("span", "lb-bundle-qty-badge", {
|
|
283
|
+
"data-qty-badge": ""
|
|
284
|
+
});
|
|
285
|
+
qtyBadgeRef.textContent = String(state.qty);
|
|
286
|
+
thumb.appendChild(qtyBadgeRef);
|
|
287
|
+
}
|
|
288
|
+
rowEl.appendChild(thumb);
|
|
289
|
+
const info = el("div", "lb-bundle-product-info");
|
|
290
|
+
const name = document.createElement("a");
|
|
291
|
+
name.className = "lb-bundle-product-name";
|
|
292
|
+
name.href = `/products/${state.product.handle}`;
|
|
293
|
+
name.textContent = state.product.title;
|
|
294
|
+
info.appendChild(name);
|
|
295
|
+
if (state.isOos) {
|
|
296
|
+
const oosLabel = el("span", "lb-bundle-oos-label");
|
|
297
|
+
oosLabel.textContent = "Out of stock";
|
|
298
|
+
info.appendChild(oosLabel);
|
|
299
|
+
} else if (state.selected) {
|
|
300
|
+
const prices = el("span", "lb-bundle-product-prices");
|
|
301
|
+
const compare = el("span", "lb-bundle-product-compare-price", {
|
|
302
|
+
"data-product-compare-price": ""
|
|
303
|
+
});
|
|
304
|
+
const priceEl = el("span", "lb-bundle-product-price", {
|
|
305
|
+
"data-product-price": ""
|
|
306
|
+
});
|
|
307
|
+
prices.appendChild(compare);
|
|
308
|
+
prices.appendChild(priceEl);
|
|
309
|
+
info.appendChild(prices);
|
|
310
|
+
const applyVariantToRow = (variant) => {
|
|
311
|
+
const unit = parseCents(variant.price.amount);
|
|
312
|
+
priceEl.textContent = formatCents(unit, currency);
|
|
313
|
+
if (variant.compareAtPrice) {
|
|
314
|
+
const cmp = parseCents(variant.compareAtPrice.amount);
|
|
315
|
+
if (cmp > unit) {
|
|
316
|
+
compare.textContent = formatCents(cmp, currency);
|
|
317
|
+
compare.removeAttribute("hidden");
|
|
318
|
+
} else {
|
|
319
|
+
compare.setAttribute("hidden", "");
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
compare.setAttribute("hidden", "");
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
applyVariantToRow(state.selected);
|
|
326
|
+
if (state.eligibleVariants.length > 1) {
|
|
327
|
+
const select = document.createElement("select");
|
|
328
|
+
select.className = "lb-bundle-variant-select";
|
|
329
|
+
select.setAttribute("data-variant-select", "");
|
|
330
|
+
select.setAttribute(
|
|
331
|
+
"aria-label",
|
|
332
|
+
`Select variant for ${state.product.title}`
|
|
333
|
+
);
|
|
334
|
+
state.eligibleVariants.forEach((variant) => {
|
|
335
|
+
const opt = document.createElement("option");
|
|
336
|
+
opt.value = variant.id;
|
|
337
|
+
opt.textContent = variant.title;
|
|
338
|
+
if (variant.id === state.selected?.id) opt.selected = true;
|
|
339
|
+
select.appendChild(opt);
|
|
340
|
+
});
|
|
341
|
+
select.addEventListener("change", () => {
|
|
342
|
+
const variant = state.eligibleVariants.find(
|
|
343
|
+
(v) => v.id === select.value
|
|
344
|
+
);
|
|
345
|
+
if (!variant) return;
|
|
346
|
+
state.selected = variant;
|
|
347
|
+
state.qty = qtyFor(state.product.id, variant.id);
|
|
348
|
+
if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);
|
|
349
|
+
applyVariantToRow(variant);
|
|
350
|
+
onVariantChange();
|
|
351
|
+
});
|
|
352
|
+
info.appendChild(select);
|
|
353
|
+
} else if (state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1) {
|
|
354
|
+
const badge = el("span", "lb-bundle-variant-badge");
|
|
355
|
+
badge.textContent = state.eligibleVariants[0].title;
|
|
356
|
+
info.appendChild(badge);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
rowEl.appendChild(info);
|
|
360
|
+
return { el: rowEl, state };
|
|
361
|
+
}
|
|
362
|
+
function renderPricingRow(bundle) {
|
|
363
|
+
const row = el("div", "lb-bundle-pricing");
|
|
364
|
+
const label = el("span", "lb-bundle-pricing__label");
|
|
365
|
+
label.textContent = "Bundle price";
|
|
366
|
+
row.appendChild(label);
|
|
367
|
+
const prices = el("span", "lb-bundle-pricing__prices");
|
|
368
|
+
const compare = el("span", "lb-bundle-compare-price", {
|
|
369
|
+
"data-compare-price": ""
|
|
370
|
+
});
|
|
371
|
+
compare.style.display = "none";
|
|
372
|
+
prices.appendChild(compare);
|
|
373
|
+
const sale = el("span", "lb-bundle-sale-price", {
|
|
374
|
+
"data-sale-price": ""
|
|
375
|
+
});
|
|
376
|
+
prices.appendChild(sale);
|
|
377
|
+
row.appendChild(prices);
|
|
378
|
+
return {
|
|
379
|
+
el: row,
|
|
380
|
+
update({ totalCents, saleCents, savingsCents, currency }) {
|
|
381
|
+
sale.textContent = formatCents(saleCents, currency);
|
|
382
|
+
if (bundle.widgetConfig.pricing.showCompareAtPrice && savingsCents > 0) {
|
|
383
|
+
compare.textContent = formatCents(totalCents, currency);
|
|
384
|
+
compare.style.display = "";
|
|
385
|
+
} else {
|
|
386
|
+
compare.style.display = "none";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function renderSavingsBar() {
|
|
392
|
+
const bar = el("div", "lb-bundle-savings-bar", {
|
|
393
|
+
"data-savings-bar": ""
|
|
394
|
+
});
|
|
395
|
+
const label = document.createElement("span");
|
|
396
|
+
label.textContent = "You save";
|
|
397
|
+
bar.appendChild(label);
|
|
398
|
+
const amount = el("span", "", { "data-savings-amount": "" });
|
|
399
|
+
bar.appendChild(amount);
|
|
400
|
+
return {
|
|
401
|
+
el: bar,
|
|
402
|
+
update({ savingsCents, currency }) {
|
|
403
|
+
if (savingsCents <= 0) {
|
|
404
|
+
bar.style.display = "none";
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
bar.style.display = "";
|
|
408
|
+
amount.textContent = formatCents(savingsCents, currency);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function renderCta(bundle, oosCount, onClick) {
|
|
58
413
|
const button = document.createElement("button");
|
|
59
|
-
button.
|
|
60
|
-
button.
|
|
61
|
-
button.setAttribute("
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
414
|
+
button.type = "button";
|
|
415
|
+
button.className = "lb-bundle-cta";
|
|
416
|
+
button.setAttribute("data-add-bundle", "");
|
|
417
|
+
if (oosCount > 0) {
|
|
418
|
+
button.disabled = true;
|
|
419
|
+
button.textContent = `${oosCount} item${oosCount === 1 ? "" : "s"} out of stock`;
|
|
420
|
+
} else {
|
|
421
|
+
button.textContent = bundle.widgetConfig.cta.ctaText || "Add to cart";
|
|
422
|
+
button.addEventListener("click", () => {
|
|
423
|
+
if (button.disabled) return;
|
|
424
|
+
onClick();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return button;
|
|
428
|
+
}
|
|
429
|
+
function computeSale(totalCents, discount, rows) {
|
|
430
|
+
if (discount.discountType === "percentage") {
|
|
431
|
+
let saleCents = 0;
|
|
432
|
+
for (const r of rows) {
|
|
433
|
+
if (!r.selected) continue;
|
|
434
|
+
const unit = parseCents(r.selected.price.amount);
|
|
435
|
+
const off = Math.floor(unit * discount.discountValue / 100);
|
|
436
|
+
const perUnit = Math.max(0, unit - off);
|
|
437
|
+
saleCents += perUnit * r.qty;
|
|
438
|
+
}
|
|
439
|
+
return saleCents;
|
|
440
|
+
}
|
|
441
|
+
return Math.max(0, totalCents - Math.round(discount.discountValue * 100));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/renderers/mix-match.ts
|
|
445
|
+
var PLACEHOLDER_THUMB_SVG2 = `
|
|
446
|
+
<svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
447
|
+
<rect x="4" y="4" width="20" height="20" rx="3"></rect>
|
|
448
|
+
<line x1="4" y1="20" x2="24" y2="20"></line>
|
|
449
|
+
<circle cx="10" cy="12" r="2"></circle>
|
|
450
|
+
</svg>`;
|
|
451
|
+
var PLUS_ICON_SVG = `
|
|
452
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
453
|
+
<line x1="9" y1="3" x2="9" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
454
|
+
<line x1="3" y1="9" x2="15" y2="9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
455
|
+
</svg>`;
|
|
456
|
+
var CLOSE_ICON_SVG = `
|
|
457
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
458
|
+
<line x1="5" y1="5" x2="15" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
459
|
+
<line x1="15" y1="5" x2="5" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
460
|
+
</svg>`;
|
|
461
|
+
var SEARCH_CLEAR_ICON_SVG = `
|
|
462
|
+
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
463
|
+
<path d="M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z"/>
|
|
464
|
+
</svg>`;
|
|
465
|
+
function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
466
|
+
const wc = bundle.widgetConfig;
|
|
467
|
+
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
468
|
+
const requiredQty = bundle.minQuantity ?? 1;
|
|
469
|
+
const maxQty = bundle.maxQuantity ?? requiredQty;
|
|
470
|
+
const eligible = buildEligibleProducts(bundle, wc.outOfStockBehavior);
|
|
471
|
+
const inStockCount = eligible.filter((e) => !e.isOos).length;
|
|
472
|
+
if (inStockCount < requiredQty) return;
|
|
473
|
+
const selections = [];
|
|
474
|
+
const root = el("div", "lb-mix-match", {
|
|
475
|
+
"data-required-quantity": String(requiredQty),
|
|
476
|
+
"data-max-quantity": String(maxQty)
|
|
477
|
+
});
|
|
478
|
+
const header = renderHeader2(bundle);
|
|
479
|
+
root.appendChild(header);
|
|
480
|
+
if (wc.countdown.showCountdown && bundle.endsAt) {
|
|
481
|
+
const countdown = renderCountdown(bundle.endsAt);
|
|
482
|
+
if (countdown) {
|
|
483
|
+
root.appendChild(countdown.el);
|
|
484
|
+
onCleanup?.(countdown.stop);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const progress = renderProgress(requiredQty);
|
|
488
|
+
root.appendChild(progress.el);
|
|
489
|
+
const slotsContainer = el("div", "lb-mix-match__slots", {
|
|
490
|
+
"data-selection-slots": ""
|
|
491
|
+
});
|
|
492
|
+
root.appendChild(slotsContainer);
|
|
493
|
+
root.appendChild(el("div", "lb-bundle-divider"));
|
|
494
|
+
const pricingSection = renderPricingSection(wc.pricing.showCompareAtPrice);
|
|
495
|
+
root.appendChild(pricingSection.el);
|
|
496
|
+
const savingsBar = wc.savingsBar.visible ? renderSavingsBar2() : null;
|
|
497
|
+
if (savingsBar) root.appendChild(savingsBar.el);
|
|
498
|
+
const placeholder = el("div", "lb-mix-match__price-placeholder", {
|
|
499
|
+
"data-price-placeholder": ""
|
|
500
|
+
});
|
|
501
|
+
const placeholderText = el(
|
|
502
|
+
"span",
|
|
503
|
+
"lb-mix-match__price-placeholder-text"
|
|
504
|
+
);
|
|
505
|
+
placeholderText.textContent = `Select ${requiredQty} items to see price`;
|
|
506
|
+
placeholder.appendChild(placeholderText);
|
|
507
|
+
root.appendChild(placeholder);
|
|
508
|
+
const modal = renderModal(bundle, eligible, currency, {
|
|
509
|
+
showSearch: wc.showSearch,
|
|
510
|
+
onAdd: (product, variant) => addSelection(product, variant),
|
|
511
|
+
onRemove: (productId, variantId) => removeSelection(productId, variantId),
|
|
512
|
+
countFor: (productId, variantId) => selections.filter(
|
|
513
|
+
(s) => s.productId === productId && s.variantId === variantId
|
|
514
|
+
).length,
|
|
515
|
+
isOverMax: () => selections.length >= maxQty
|
|
516
|
+
});
|
|
517
|
+
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", "");
|
|
522
|
+
cta.disabled = true;
|
|
523
|
+
cta.textContent = `Select ${requiredQty} items to unlock`;
|
|
524
|
+
cta.addEventListener("click", () => {
|
|
525
|
+
if (cta.disabled) return;
|
|
526
|
+
const lines = selections.map((s) => ({
|
|
527
|
+
merchandiseId: s.variantId,
|
|
528
|
+
quantity: 1,
|
|
529
|
+
attributes: [
|
|
530
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
531
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
532
|
+
]
|
|
533
|
+
}));
|
|
534
|
+
onAddToCart(lines);
|
|
535
|
+
});
|
|
536
|
+
root.appendChild(cta);
|
|
537
|
+
root.appendChild(
|
|
538
|
+
el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
|
|
539
|
+
);
|
|
540
|
+
root.appendChild(
|
|
541
|
+
el("span", "lb-visually-hidden", {
|
|
542
|
+
"data-status": "",
|
|
543
|
+
"aria-live": "polite"
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
container.appendChild(root);
|
|
547
|
+
const firstEligible = eligible.find((ep) => !ep.isOos);
|
|
548
|
+
const firstVariant = firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];
|
|
549
|
+
if (firstEligible && firstVariant) {
|
|
550
|
+
selections.push({
|
|
551
|
+
productId: firstEligible.product.id,
|
|
552
|
+
productTitle: firstEligible.product.title,
|
|
553
|
+
variantId: firstVariant.id,
|
|
554
|
+
variantTitle: firstVariant.title,
|
|
555
|
+
imageUrl: firstEligible.product.featuredImage?.url ?? null,
|
|
556
|
+
priceCents: parseCents(firstVariant.price.amount),
|
|
557
|
+
compareCents: firstVariant.compareAtPrice ? parseCents(firstVariant.compareAtPrice.amount) : null
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
afterMutation();
|
|
561
|
+
function addSelection(product, variant) {
|
|
562
|
+
if (selections.length >= maxQty) return;
|
|
563
|
+
selections.push({
|
|
564
|
+
productId: product.id,
|
|
565
|
+
productTitle: product.title,
|
|
566
|
+
variantId: variant.id,
|
|
567
|
+
variantTitle: variant.title,
|
|
568
|
+
imageUrl: product.featuredImage?.url ?? null,
|
|
569
|
+
priceCents: parseCents(variant.price.amount),
|
|
570
|
+
compareCents: variant.compareAtPrice ? parseCents(variant.compareAtPrice.amount) : null
|
|
571
|
+
});
|
|
572
|
+
afterMutation();
|
|
573
|
+
}
|
|
574
|
+
function removeSelection(productId, variantId) {
|
|
575
|
+
const idx = selections.findIndex(
|
|
576
|
+
(s) => s.productId === productId && s.variantId === variantId
|
|
577
|
+
);
|
|
578
|
+
if (idx === -1) return;
|
|
579
|
+
selections.splice(idx, 1);
|
|
580
|
+
afterMutation();
|
|
581
|
+
}
|
|
582
|
+
function removeSlotAt(index) {
|
|
583
|
+
if (index < 0 || index >= selections.length) return;
|
|
584
|
+
selections.splice(index, 1);
|
|
585
|
+
afterMutation();
|
|
586
|
+
}
|
|
587
|
+
function afterMutation() {
|
|
588
|
+
renderSlots();
|
|
589
|
+
progress.update(selections.length);
|
|
590
|
+
pricingSection.update(selections, bundle, currency);
|
|
591
|
+
if (savingsBar) savingsBar.update(selections, bundle, currency);
|
|
592
|
+
placeholder.style.display = selections.length === 0 ? "" : "none";
|
|
593
|
+
modal.refreshCounts();
|
|
594
|
+
updateCta();
|
|
595
|
+
}
|
|
596
|
+
function renderSlots() {
|
|
597
|
+
slotsContainer.innerHTML = "";
|
|
598
|
+
const totalSlots = Math.max(requiredQty, selections.length);
|
|
599
|
+
for (let i = 0; i < totalSlots; i++) {
|
|
600
|
+
const selection = selections[i];
|
|
601
|
+
if (selection) {
|
|
602
|
+
slotsContainer.appendChild(
|
|
603
|
+
renderFilledSlot(selection, i, currency, () => removeSlotAt(i))
|
|
604
|
+
);
|
|
605
|
+
} else {
|
|
606
|
+
slotsContainer.appendChild(
|
|
607
|
+
renderEmptySlot(i, () => modal.open())
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function updateCta() {
|
|
613
|
+
const count = selections.length;
|
|
614
|
+
if (count < requiredQty) {
|
|
615
|
+
cta.disabled = true;
|
|
616
|
+
cta.textContent = `Select ${requiredQty - count} more to unlock`;
|
|
617
|
+
} else {
|
|
618
|
+
cta.disabled = false;
|
|
619
|
+
cta.textContent = wc.cta.ctaText || "Add to cart";
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function renderHeader2(bundle) {
|
|
624
|
+
const wc = bundle.widgetConfig;
|
|
625
|
+
const header = el("div", "lb-bundle-header");
|
|
626
|
+
const content = el("div", "lb-bundle-header__content");
|
|
627
|
+
const title = el("h3", "lb-bundle-title");
|
|
628
|
+
title.textContent = bundle.title;
|
|
629
|
+
content.appendChild(title);
|
|
630
|
+
header.appendChild(content);
|
|
631
|
+
if (wc.pricing.showSaveBadge) {
|
|
632
|
+
const { discountType, discountValue } = bundle.discountConfig;
|
|
633
|
+
let label = null;
|
|
634
|
+
if (discountType === "percentage" && discountValue > 0) {
|
|
635
|
+
label = `-${Math.round(discountValue)}%`;
|
|
636
|
+
} else if (discountType === "fixed_amount" && discountValue > 0) {
|
|
637
|
+
label = `-${formatCents(
|
|
638
|
+
Math.round(discountValue * 100),
|
|
639
|
+
bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
|
|
640
|
+
)}`;
|
|
641
|
+
}
|
|
642
|
+
if (label) {
|
|
643
|
+
const badge = el("span", "lb-bundle-header__badge");
|
|
644
|
+
badge.textContent = label;
|
|
645
|
+
header.appendChild(badge);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return header;
|
|
649
|
+
}
|
|
650
|
+
function renderProgress(requiredQty) {
|
|
651
|
+
const wrap = el("div", "lb-mix-match__progress");
|
|
652
|
+
const labels = el("div", "lb-mix-match__progress-labels");
|
|
653
|
+
const count = el("span", "lb-mix-match__progress-count", {
|
|
654
|
+
"data-progress-count": ""
|
|
655
|
+
});
|
|
656
|
+
count.textContent = `0 of ${requiredQty} selected`;
|
|
657
|
+
labels.appendChild(count);
|
|
658
|
+
const remaining = el("span", "lb-mix-match__progress-remaining", {
|
|
659
|
+
"data-progress-remaining": ""
|
|
660
|
+
});
|
|
661
|
+
remaining.textContent = `${requiredQty} more to go`;
|
|
662
|
+
labels.appendChild(remaining);
|
|
663
|
+
wrap.appendChild(labels);
|
|
664
|
+
const track = el("div", "lb-mix-match__progress-track", {
|
|
665
|
+
role: "progressbar",
|
|
666
|
+
"aria-valuenow": "0",
|
|
667
|
+
"aria-valuemin": "0",
|
|
668
|
+
"aria-valuemax": String(requiredQty)
|
|
669
|
+
});
|
|
670
|
+
const fill = el("div", "lb-mix-match__progress-fill", {
|
|
671
|
+
"data-progress-fill": ""
|
|
672
|
+
});
|
|
673
|
+
fill.style.width = "0%";
|
|
674
|
+
track.appendChild(fill);
|
|
675
|
+
wrap.appendChild(track);
|
|
676
|
+
function update(selected) {
|
|
677
|
+
const pct = Math.min(100, selected / requiredQty * 100);
|
|
678
|
+
count.textContent = `${selected} of ${requiredQty} selected`;
|
|
679
|
+
if (selected >= requiredQty) {
|
|
680
|
+
remaining.textContent = "Complete";
|
|
681
|
+
} else {
|
|
682
|
+
remaining.textContent = `${requiredQty - selected} more to go`;
|
|
683
|
+
}
|
|
684
|
+
fill.style.width = `${pct}%`;
|
|
685
|
+
track.setAttribute("aria-valuenow", String(Math.min(selected, requiredQty)));
|
|
686
|
+
}
|
|
687
|
+
return { el: wrap, update };
|
|
688
|
+
}
|
|
689
|
+
function renderEmptySlot(index, onClick) {
|
|
690
|
+
const slot = el(
|
|
691
|
+
"div",
|
|
692
|
+
"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--empty",
|
|
693
|
+
{
|
|
694
|
+
"data-slot": String(index + 1),
|
|
695
|
+
tabindex: "0",
|
|
696
|
+
role: "button",
|
|
697
|
+
"aria-label": "Add a product to the bundle"
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
const thumb = el("div", "lb-mix-match__empty-thumb");
|
|
701
|
+
thumb.innerHTML = PLUS_ICON_SVG;
|
|
702
|
+
slot.appendChild(thumb);
|
|
703
|
+
const text = el("span", "lb-mix-match__empty-text");
|
|
704
|
+
text.textContent = "Choose an item";
|
|
705
|
+
slot.appendChild(text);
|
|
706
|
+
slot.addEventListener("click", onClick);
|
|
707
|
+
slot.addEventListener("keydown", (e) => {
|
|
708
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
709
|
+
e.preventDefault();
|
|
710
|
+
onClick();
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return slot;
|
|
714
|
+
}
|
|
715
|
+
function renderFilledSlot(selection, index, currency, onRemove) {
|
|
716
|
+
const slot = el(
|
|
717
|
+
"div",
|
|
718
|
+
"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--filled",
|
|
719
|
+
{ "data-slot": String(index + 1) }
|
|
720
|
+
);
|
|
721
|
+
const thumb = el("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
|
|
722
|
+
if (selection.imageUrl) {
|
|
723
|
+
const img = document.createElement("img");
|
|
724
|
+
img.src = transformImageUrl(selection.imageUrl, {
|
|
725
|
+
width: THUMB_PX,
|
|
726
|
+
height: THUMB_PX,
|
|
727
|
+
crop: "center"
|
|
728
|
+
});
|
|
729
|
+
img.alt = selection.productTitle;
|
|
730
|
+
img.width = THUMB_PX;
|
|
731
|
+
img.height = THUMB_PX;
|
|
732
|
+
img.loading = "lazy";
|
|
733
|
+
thumb.appendChild(img);
|
|
734
|
+
} else {
|
|
735
|
+
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
736
|
+
}
|
|
737
|
+
slot.appendChild(thumb);
|
|
738
|
+
const info = el("div", "lb-mix-match__filled-info");
|
|
739
|
+
const title = el("span", "lb-mix-match__filled-title");
|
|
740
|
+
title.textContent = selection.productTitle;
|
|
741
|
+
info.appendChild(title);
|
|
742
|
+
if (selection.variantTitle && selection.variantTitle !== "Default Title") {
|
|
743
|
+
const variant = el("span", "lb-mix-match__filled-variant");
|
|
744
|
+
variant.textContent = selection.variantTitle;
|
|
745
|
+
info.appendChild(variant);
|
|
746
|
+
}
|
|
747
|
+
const priceWrap = el("span", "lb-mix-match__filled-price");
|
|
748
|
+
if (selection.compareCents && selection.compareCents > selection.priceCents) {
|
|
749
|
+
const compare = el("span", "lb-mix-match__filled-compare");
|
|
750
|
+
compare.textContent = formatCents(selection.compareCents, currency);
|
|
751
|
+
priceWrap.appendChild(compare);
|
|
752
|
+
}
|
|
753
|
+
const priceEl = document.createElement("span");
|
|
754
|
+
priceEl.textContent = formatCents(selection.priceCents, currency);
|
|
755
|
+
priceWrap.appendChild(priceEl);
|
|
756
|
+
info.appendChild(priceWrap);
|
|
757
|
+
slot.appendChild(info);
|
|
758
|
+
const remove = document.createElement("button");
|
|
759
|
+
remove.type = "button";
|
|
760
|
+
remove.className = "lb-mix-match__slot-remove";
|
|
761
|
+
remove.setAttribute("aria-label", `Remove ${selection.productTitle}`);
|
|
762
|
+
remove.innerHTML = CLOSE_ICON_SVG;
|
|
763
|
+
remove.addEventListener("click", (e) => {
|
|
764
|
+
e.stopPropagation();
|
|
765
|
+
onRemove();
|
|
766
|
+
});
|
|
767
|
+
slot.appendChild(remove);
|
|
768
|
+
return slot;
|
|
769
|
+
}
|
|
770
|
+
function renderPricingSection(showCompareAtPrice) {
|
|
771
|
+
const wrap = el("div", "lb-bundle-pricing", { "data-pricing-section": "" });
|
|
772
|
+
wrap.style.display = "none";
|
|
773
|
+
const label = el("span", "lb-bundle-pricing__label");
|
|
774
|
+
label.textContent = "Bundle price";
|
|
775
|
+
wrap.appendChild(label);
|
|
776
|
+
const prices = el("span", "lb-bundle-pricing__prices");
|
|
777
|
+
const compare = el("span", "lb-bundle-compare-price", {
|
|
778
|
+
"data-compare-price": ""
|
|
779
|
+
});
|
|
780
|
+
if (showCompareAtPrice) prices.appendChild(compare);
|
|
781
|
+
const sale = el("span", "lb-bundle-sale-price", { "data-sale-price": "" });
|
|
782
|
+
prices.appendChild(sale);
|
|
783
|
+
wrap.appendChild(prices);
|
|
784
|
+
function update(selections, bundle, currency) {
|
|
785
|
+
if (selections.length === 0) {
|
|
786
|
+
wrap.style.display = "none";
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
wrap.style.display = "";
|
|
790
|
+
const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
|
|
791
|
+
const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
|
|
792
|
+
if (showCompareAtPrice && totalCents > saleCents) {
|
|
793
|
+
compare.textContent = formatCents(totalCents, currency);
|
|
794
|
+
compare.style.display = "";
|
|
795
|
+
} else {
|
|
796
|
+
compare.style.display = "none";
|
|
797
|
+
}
|
|
798
|
+
sale.textContent = formatCents(saleCents, currency);
|
|
799
|
+
}
|
|
800
|
+
return { el: wrap, update };
|
|
801
|
+
}
|
|
802
|
+
function renderSavingsBar2() {
|
|
803
|
+
const wrap = el("div", "lb-bundle-savings-bar", {
|
|
804
|
+
"data-savings-bar": ""
|
|
805
|
+
});
|
|
806
|
+
wrap.style.display = "none";
|
|
807
|
+
const labelEl = document.createElement("span");
|
|
808
|
+
labelEl.textContent = "You save";
|
|
809
|
+
wrap.appendChild(labelEl);
|
|
810
|
+
const amount = el("span", "", { "data-savings-amount": "" });
|
|
811
|
+
wrap.appendChild(amount);
|
|
812
|
+
function update(selections, bundle, currency) {
|
|
813
|
+
if (selections.length === 0) {
|
|
814
|
+
wrap.style.display = "none";
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
|
|
818
|
+
const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
|
|
819
|
+
const savings = Math.max(0, totalCents - saleCents);
|
|
820
|
+
if (savings <= 0) {
|
|
821
|
+
wrap.style.display = "none";
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
wrap.style.display = "";
|
|
825
|
+
amount.textContent = formatCents(savings, currency);
|
|
826
|
+
}
|
|
827
|
+
return { el: wrap, update };
|
|
828
|
+
}
|
|
829
|
+
function renderModal(bundle, eligible, currency, handlers) {
|
|
830
|
+
const overlay = el("div", "lb-mix-match__modal-overlay", {
|
|
831
|
+
"data-modal-overlay": "",
|
|
832
|
+
"data-bundle-gid": bundle.id
|
|
833
|
+
});
|
|
834
|
+
overlay.style.display = "none";
|
|
835
|
+
const modal = el("div", "lb-mix-match__modal", {
|
|
836
|
+
role: "dialog",
|
|
837
|
+
"aria-modal": "true",
|
|
838
|
+
"aria-labelledby": `lb-modal-title-${sanitizeId(bundle.id)}`,
|
|
839
|
+
tabindex: "-1"
|
|
840
|
+
});
|
|
841
|
+
const modalHeader = el("div", "lb-mix-match__modal-header");
|
|
842
|
+
const modalTitle = el("h4", "lb-mix-match__modal-title", {
|
|
843
|
+
id: `lb-modal-title-${sanitizeId(bundle.id)}`
|
|
844
|
+
});
|
|
845
|
+
modalTitle.textContent = "Pick an item";
|
|
846
|
+
modalHeader.appendChild(modalTitle);
|
|
847
|
+
const closeBtn = document.createElement("button");
|
|
848
|
+
closeBtn.type = "button";
|
|
849
|
+
closeBtn.className = "lb-mix-match__modal-close";
|
|
850
|
+
closeBtn.setAttribute("data-modal-close", "");
|
|
851
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
852
|
+
closeBtn.innerHTML = CLOSE_ICON_SVG;
|
|
853
|
+
closeBtn.addEventListener("click", close);
|
|
854
|
+
modalHeader.appendChild(closeBtn);
|
|
855
|
+
modal.appendChild(modalHeader);
|
|
856
|
+
let searchInput = null;
|
|
857
|
+
let searchClearBtn = null;
|
|
858
|
+
if (handlers.showSearch) {
|
|
859
|
+
const searchWrap = el("div", "lb-mix-match__modal-search");
|
|
860
|
+
searchInput = document.createElement("input");
|
|
861
|
+
searchInput.type = "text";
|
|
862
|
+
searchInput.className = "lb-mix-match__modal-search-input";
|
|
863
|
+
searchInput.setAttribute("data-modal-search", "");
|
|
864
|
+
searchInput.setAttribute("role", "searchbox");
|
|
865
|
+
searchInput.setAttribute("aria-label", "Search products");
|
|
866
|
+
searchInput.setAttribute("placeholder", "Search products");
|
|
867
|
+
searchInput.autocomplete = "off";
|
|
868
|
+
searchInput.addEventListener("input", () => applySearch());
|
|
869
|
+
searchWrap.appendChild(searchInput);
|
|
870
|
+
searchClearBtn = document.createElement("button");
|
|
871
|
+
searchClearBtn.type = "button";
|
|
872
|
+
searchClearBtn.className = "lb-mix-match__modal-search-clear";
|
|
873
|
+
searchClearBtn.setAttribute("data-modal-search-clear", "");
|
|
874
|
+
searchClearBtn.setAttribute("aria-label", "Clear search");
|
|
875
|
+
searchClearBtn.style.display = "none";
|
|
876
|
+
searchClearBtn.innerHTML = SEARCH_CLEAR_ICON_SVG;
|
|
877
|
+
searchClearBtn.addEventListener("click", () => {
|
|
878
|
+
if (!searchInput) return;
|
|
879
|
+
searchInput.value = "";
|
|
880
|
+
applySearch();
|
|
881
|
+
searchInput.focus();
|
|
882
|
+
});
|
|
883
|
+
searchWrap.appendChild(searchClearBtn);
|
|
884
|
+
modal.appendChild(searchWrap);
|
|
885
|
+
}
|
|
886
|
+
const list = el("div", "lb-mix-match__modal-list", {
|
|
887
|
+
"data-modal-list": ""
|
|
888
|
+
});
|
|
889
|
+
modal.appendChild(list);
|
|
890
|
+
const empty = el("div", "lb-mix-match__modal-empty", {
|
|
891
|
+
"data-modal-empty": ""
|
|
892
|
+
});
|
|
893
|
+
empty.style.display = "none";
|
|
894
|
+
const emptyText = document.createElement("p");
|
|
895
|
+
emptyText.textContent = "No products match your search.";
|
|
896
|
+
empty.appendChild(emptyText);
|
|
897
|
+
modal.appendChild(empty);
|
|
898
|
+
const live = el("span", "lb-visually-hidden", {
|
|
899
|
+
"data-modal-live": "",
|
|
900
|
+
"aria-live": "polite"
|
|
901
|
+
});
|
|
902
|
+
modal.appendChild(live);
|
|
903
|
+
overlay.appendChild(modal);
|
|
904
|
+
let rowsBuilt = false;
|
|
905
|
+
const productRows = [];
|
|
906
|
+
function buildRows() {
|
|
907
|
+
if (rowsBuilt) return;
|
|
908
|
+
rowsBuilt = true;
|
|
909
|
+
list.innerHTML = "";
|
|
910
|
+
eligible.forEach((ep) => {
|
|
911
|
+
const variant = ep.firstAvailableVariant ?? ep.variants[0];
|
|
912
|
+
if (!variant) return;
|
|
913
|
+
const productEl = el(
|
|
914
|
+
"div",
|
|
915
|
+
ep.isOos ? "lb-mix-match__modal-product lb-mix-match__modal-product--sold-out" : "lb-mix-match__modal-product",
|
|
916
|
+
{ "data-product-id": ep.product.id.replace(/^.*\//, "") }
|
|
917
|
+
);
|
|
918
|
+
const thumb = el("div", "lb-mix-match__modal-product-thumb");
|
|
919
|
+
if (ep.product.featuredImage) {
|
|
920
|
+
const img = document.createElement("img");
|
|
921
|
+
img.src = transformImageUrl(ep.product.featuredImage.url, {
|
|
922
|
+
width: THUMB_PX,
|
|
923
|
+
height: THUMB_PX,
|
|
924
|
+
crop: "center"
|
|
925
|
+
});
|
|
926
|
+
img.alt = ep.product.featuredImage.altText ?? ep.product.title;
|
|
927
|
+
img.width = THUMB_PX;
|
|
928
|
+
img.height = THUMB_PX;
|
|
929
|
+
img.loading = "lazy";
|
|
930
|
+
thumb.appendChild(img);
|
|
931
|
+
} else {
|
|
932
|
+
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
933
|
+
}
|
|
934
|
+
productEl.appendChild(thumb);
|
|
935
|
+
const info = el("div", "lb-mix-match__modal-product-info");
|
|
936
|
+
const title = el("span", "lb-mix-match__modal-product-title");
|
|
937
|
+
title.textContent = ep.product.title;
|
|
938
|
+
info.appendChild(title);
|
|
939
|
+
const price = el("span", "lb-mix-match__modal-product-price");
|
|
940
|
+
price.textContent = formatCents(parseCents(variant.price.amount), currency);
|
|
941
|
+
info.appendChild(price);
|
|
942
|
+
if (ep.isOos) {
|
|
943
|
+
const soldOut = el("span", "lb-mix-match__modal-sold-out-label");
|
|
944
|
+
soldOut.textContent = "Sold out";
|
|
945
|
+
info.appendChild(soldOut);
|
|
946
|
+
}
|
|
947
|
+
productEl.appendChild(info);
|
|
948
|
+
if (!ep.isOos) {
|
|
949
|
+
const addBtn = document.createElement("button");
|
|
950
|
+
addBtn.type = "button";
|
|
951
|
+
addBtn.className = "lb-mix-match__modal-add";
|
|
952
|
+
addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
|
|
953
|
+
addBtn.innerHTML = PLUS_ICON_SVG;
|
|
954
|
+
const countBadge = el("span", "lb-bundle-qty-badge");
|
|
955
|
+
countBadge.style.display = "none";
|
|
956
|
+
addBtn.appendChild(countBadge);
|
|
957
|
+
addBtn.addEventListener("click", () => {
|
|
958
|
+
if (handlers.isOverMax()) return;
|
|
959
|
+
handlers.onAdd(ep.product, variant);
|
|
960
|
+
});
|
|
961
|
+
productEl.appendChild(addBtn);
|
|
962
|
+
const removeBtn = document.createElement("button");
|
|
963
|
+
removeBtn.type = "button";
|
|
964
|
+
removeBtn.className = "lb-mix-match__slot-remove";
|
|
965
|
+
removeBtn.setAttribute("aria-label", `Remove one ${ep.product.title}`);
|
|
966
|
+
removeBtn.style.display = "none";
|
|
967
|
+
removeBtn.innerHTML = CLOSE_ICON_SVG;
|
|
968
|
+
removeBtn.addEventListener("click", (e) => {
|
|
969
|
+
e.stopPropagation();
|
|
970
|
+
handlers.onRemove(ep.product.id, variant.id);
|
|
971
|
+
});
|
|
972
|
+
productEl.appendChild(removeBtn);
|
|
973
|
+
productRows.push({
|
|
974
|
+
el: productEl,
|
|
975
|
+
product: ep.product,
|
|
976
|
+
variant,
|
|
977
|
+
updateCount: () => {
|
|
978
|
+
const count = handlers.countFor(ep.product.id, variant.id);
|
|
979
|
+
if (count > 0) {
|
|
980
|
+
countBadge.textContent = String(count);
|
|
981
|
+
countBadge.style.display = "";
|
|
982
|
+
removeBtn.style.display = "";
|
|
983
|
+
} else {
|
|
984
|
+
countBadge.style.display = "none";
|
|
985
|
+
removeBtn.style.display = "none";
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
} else {
|
|
990
|
+
productRows.push({
|
|
991
|
+
el: productEl,
|
|
992
|
+
product: ep.product,
|
|
993
|
+
variant,
|
|
994
|
+
updateCount: () => {
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
list.appendChild(productEl);
|
|
999
|
+
});
|
|
1000
|
+
refreshCounts();
|
|
1001
|
+
}
|
|
1002
|
+
function applySearch() {
|
|
1003
|
+
if (!searchInput) return;
|
|
1004
|
+
const query = searchInput.value.trim().toLowerCase();
|
|
1005
|
+
if (searchClearBtn) {
|
|
1006
|
+
searchClearBtn.style.display = query ? "" : "none";
|
|
1007
|
+
}
|
|
1008
|
+
let visibleCount = 0;
|
|
1009
|
+
productRows.forEach((row) => {
|
|
1010
|
+
const match = !query || row.product.title.toLowerCase().includes(query);
|
|
1011
|
+
row.el.style.display = match ? "" : "none";
|
|
1012
|
+
if (match) visibleCount++;
|
|
1013
|
+
});
|
|
1014
|
+
empty.style.display = visibleCount === 0 && query ? "" : "none";
|
|
1015
|
+
}
|
|
1016
|
+
let lastFocused = null;
|
|
1017
|
+
function onKeydown(e) {
|
|
1018
|
+
if (e.key === "Escape") {
|
|
1019
|
+
e.preventDefault();
|
|
1020
|
+
close();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (e.key === "Tab") {
|
|
1024
|
+
trapFocus(e, modal);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
let isOpen = false;
|
|
1028
|
+
function open() {
|
|
1029
|
+
if (isOpen) return;
|
|
1030
|
+
if (handlers.isOverMax()) return;
|
|
1031
|
+
isOpen = true;
|
|
1032
|
+
buildRows();
|
|
1033
|
+
lastFocused = overlay.getRootNode().activeElement;
|
|
1034
|
+
overlay.style.display = "";
|
|
1035
|
+
overlay.classList.add("lb-mix-match__modal-overlay--open");
|
|
1036
|
+
modal.focus();
|
|
1037
|
+
document.addEventListener("keydown", onKeydown);
|
|
1038
|
+
overlay.addEventListener("click", onOverlayClick);
|
|
1039
|
+
}
|
|
1040
|
+
function close() {
|
|
1041
|
+
if (!isOpen) return;
|
|
1042
|
+
isOpen = false;
|
|
1043
|
+
overlay.classList.remove("lb-mix-match__modal-overlay--open");
|
|
1044
|
+
overlay.style.display = "none";
|
|
1045
|
+
document.removeEventListener("keydown", onKeydown);
|
|
1046
|
+
overlay.removeEventListener("click", onOverlayClick);
|
|
1047
|
+
if (lastFocused instanceof HTMLElement) {
|
|
1048
|
+
lastFocused.focus();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function onOverlayClick(e) {
|
|
1052
|
+
if (e.target === overlay) close();
|
|
1053
|
+
}
|
|
1054
|
+
function refreshCounts() {
|
|
1055
|
+
productRows.forEach((r) => r.updateCount());
|
|
1056
|
+
}
|
|
1057
|
+
return { el: overlay, open, close, refreshCounts };
|
|
1058
|
+
}
|
|
1059
|
+
function buildEligibleProducts(bundle, oosBehavior) {
|
|
1060
|
+
const result = [];
|
|
1061
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1062
|
+
for (const product of bundle.products) {
|
|
1063
|
+
if (seen.has(product.id)) continue;
|
|
1064
|
+
seen.add(product.id);
|
|
1065
|
+
const available = product.variants.nodes.filter((v) => v.availableForSale);
|
|
1066
|
+
const isOos = available.length === 0;
|
|
1067
|
+
if (isOos && oosBehavior === "hide") continue;
|
|
1068
|
+
result.push({
|
|
1069
|
+
product,
|
|
1070
|
+
variants: product.variants.nodes,
|
|
1071
|
+
firstAvailableVariant: available[0] ?? null,
|
|
1072
|
+
isOos
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
return result;
|
|
1076
|
+
}
|
|
1077
|
+
function trapFocus(e, container) {
|
|
1078
|
+
const focusables = container.querySelectorAll(
|
|
1079
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
1080
|
+
);
|
|
1081
|
+
if (focusables.length === 0) return;
|
|
1082
|
+
const first = focusables[0];
|
|
1083
|
+
const last = focusables[focusables.length - 1];
|
|
1084
|
+
const active = container.getRootNode().activeElement;
|
|
1085
|
+
if (e.shiftKey && active === first) {
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
last.focus();
|
|
1088
|
+
} else if (!e.shiftKey && active === last) {
|
|
1089
|
+
e.preventDefault();
|
|
1090
|
+
first.focus();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function sanitizeId(gid) {
|
|
1094
|
+
return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/renderers/volume.ts
|
|
1098
|
+
function renderVolumeBundle(container, bundle, onAddToCart, onCleanup) {
|
|
1099
|
+
const wc = bundle.widgetConfig;
|
|
1100
|
+
const product = bundle.products[0];
|
|
1101
|
+
const variant = product?.variants.nodes.find((v) => v.availableForSale);
|
|
1102
|
+
if (!variant && wc.outOfStockBehavior === "hide") return;
|
|
1103
|
+
const basePriceCents = variant ? parseCents(variant.price.amount) : 0;
|
|
1104
|
+
const currency = variant?.price.currencyCode ?? "USD";
|
|
1105
|
+
const discountType = bundle.discountConfig.discountType;
|
|
1106
|
+
const resolved = bundle.volumeTiers.map((tier, index) => {
|
|
1107
|
+
let perUnit;
|
|
1108
|
+
if (discountType === "fixed_amount") {
|
|
1109
|
+
const amt = Math.round((tier.amount ?? 0) * 100);
|
|
1110
|
+
perUnit = Math.max(0, basePriceCents - amt);
|
|
1111
|
+
} else {
|
|
1112
|
+
const pct = tier.percentage ?? 0;
|
|
1113
|
+
const discount = Math.floor(basePriceCents * pct / 100);
|
|
1114
|
+
perUnit = Math.max(0, basePriceCents - discount);
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
tier,
|
|
1118
|
+
index,
|
|
1119
|
+
qty: tier.minQuantity,
|
|
1120
|
+
pricePerUnitCents: perUnit,
|
|
1121
|
+
basePricePerUnitCents: basePriceCents
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1124
|
+
const bestTierIndex = pickBestTierIndex(resolved);
|
|
1125
|
+
let selectedIndex = wc.defaultTier === "best_value" ? bestTierIndex : 0;
|
|
1126
|
+
if (typeof wc.defaultTier === "number") {
|
|
1127
|
+
selectedIndex = clamp(wc.defaultTier, 0, resolved.length - 1);
|
|
1128
|
+
}
|
|
1129
|
+
const popularIndex = wc.popularBadge.tierIndex !== void 0 ? clamp(wc.popularBadge.tierIndex, 0, resolved.length - 1) : bestTierIndex;
|
|
1130
|
+
const root = el("div", "lb-volume");
|
|
1131
|
+
root.appendChild(
|
|
1132
|
+
renderHeader3(bundle, resolved, selectedIndex, currency, discountType)
|
|
1133
|
+
);
|
|
1134
|
+
if (wc.countdown.showCountdown && bundle.endsAt) {
|
|
1135
|
+
const countdown = renderCountdown(bundle.endsAt);
|
|
1136
|
+
if (countdown) {
|
|
1137
|
+
root.appendChild(countdown.el);
|
|
1138
|
+
onCleanup?.(countdown.stop);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const tierGroup = el("div", "lb-volume__tiers", {
|
|
1142
|
+
role: "radiogroup",
|
|
1143
|
+
"aria-label": "Quantity tiers",
|
|
1144
|
+
"data-tier-group": ""
|
|
1145
|
+
});
|
|
1146
|
+
resolved.forEach((r) => {
|
|
1147
|
+
const tierEl = renderTierCard(
|
|
1148
|
+
r,
|
|
1149
|
+
r.index === selectedIndex,
|
|
1150
|
+
currency,
|
|
1151
|
+
wc.popularBadge.visible && r.index === popularIndex ? wc.popularBadge.text : null,
|
|
1152
|
+
wc.pricing.showComparePrice,
|
|
1153
|
+
wc.pricing.showPerUnitPrice
|
|
1154
|
+
);
|
|
1155
|
+
tierEl.addEventListener("click", () => selectTier(r.index));
|
|
1156
|
+
tierEl.addEventListener("keydown", (e) => {
|
|
1157
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1158
|
+
e.preventDefault();
|
|
1159
|
+
selectTier(r.index);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
1163
|
+
e.preventDefault();
|
|
1164
|
+
const delta = e.key === "ArrowDown" || e.key === "ArrowRight" ? 1 : -1;
|
|
1165
|
+
const next = (r.index + delta + resolved.length) % resolved.length;
|
|
1166
|
+
selectTier(next);
|
|
1167
|
+
const target = tierGroup.children[next];
|
|
1168
|
+
target?.focus();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
tierGroup.appendChild(tierEl);
|
|
1172
|
+
});
|
|
1173
|
+
root.appendChild(tierGroup);
|
|
1174
|
+
root.appendChild(el("div", "lb-bundle-divider"));
|
|
1175
|
+
let pricingEl = renderPricingRow2(
|
|
1176
|
+
resolved,
|
|
1177
|
+
selectedIndex,
|
|
1178
|
+
currency,
|
|
1179
|
+
wc.pricing.showItemCount,
|
|
1180
|
+
wc.pricing.showCompareAtPrice
|
|
1181
|
+
);
|
|
1182
|
+
root.appendChild(pricingEl);
|
|
1183
|
+
let savingsBarEl = wc.savingsBar.visible ? renderSavingsBar3(resolved, selectedIndex, currency) : null;
|
|
1184
|
+
if (savingsBarEl) root.appendChild(savingsBarEl);
|
|
1185
|
+
const cta = renderCta2(bundle, () => {
|
|
1186
|
+
if (!variant) return;
|
|
1187
|
+
const r = resolved[selectedIndex];
|
|
1188
|
+
if (!r) return;
|
|
1189
|
+
onAddToCart([
|
|
1190
|
+
{
|
|
66
1191
|
merchandiseId: variant.id,
|
|
67
|
-
quantity:
|
|
1192
|
+
quantity: r.qty,
|
|
68
1193
|
attributes: [
|
|
69
1194
|
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
70
1195
|
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
71
1196
|
]
|
|
72
|
-
}
|
|
1197
|
+
}
|
|
1198
|
+
]);
|
|
1199
|
+
});
|
|
1200
|
+
root.appendChild(cta);
|
|
1201
|
+
root.appendChild(
|
|
1202
|
+
el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
|
|
1203
|
+
);
|
|
1204
|
+
root.appendChild(
|
|
1205
|
+
el("span", "lb-visually-hidden", {
|
|
1206
|
+
"data-status": "",
|
|
1207
|
+
"aria-live": "polite"
|
|
1208
|
+
})
|
|
1209
|
+
);
|
|
1210
|
+
container.appendChild(root);
|
|
1211
|
+
function selectTier(idx) {
|
|
1212
|
+
if (idx === selectedIndex || idx < 0 || idx >= resolved.length) return;
|
|
1213
|
+
selectedIndex = idx;
|
|
1214
|
+
Array.from(tierGroup.children).forEach((card, i) => {
|
|
1215
|
+
card.setAttribute("aria-checked", String(i === idx));
|
|
1216
|
+
card.tabIndex = i === idx ? 0 : -1;
|
|
73
1217
|
});
|
|
74
|
-
|
|
75
|
-
|
|
1218
|
+
const newPricing = renderPricingRow2(
|
|
1219
|
+
resolved,
|
|
1220
|
+
idx,
|
|
1221
|
+
currency,
|
|
1222
|
+
wc.pricing.showItemCount,
|
|
1223
|
+
wc.pricing.showCompareAtPrice
|
|
1224
|
+
);
|
|
1225
|
+
pricingEl.replaceWith(newPricing);
|
|
1226
|
+
pricingEl = newPricing;
|
|
1227
|
+
if (savingsBarEl) {
|
|
1228
|
+
const newBar = renderSavingsBar3(resolved, idx, currency);
|
|
1229
|
+
savingsBarEl.replaceWith(newBar);
|
|
1230
|
+
savingsBarEl = newBar;
|
|
1231
|
+
}
|
|
1232
|
+
const badgeEl = root.querySelector("[data-header-badge]");
|
|
1233
|
+
if (badgeEl) {
|
|
1234
|
+
const label = badgeFor(resolved[idx], currency, discountType);
|
|
1235
|
+
if (label) {
|
|
1236
|
+
badgeEl.textContent = label;
|
|
1237
|
+
badgeEl.style.display = "";
|
|
1238
|
+
} else {
|
|
1239
|
+
badgeEl.style.display = "none";
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function renderHeader3(bundle, resolved, selectedIndex, currency, discountType) {
|
|
1245
|
+
const wc = bundle.widgetConfig;
|
|
1246
|
+
const header = el("div", "lb-bundle-header");
|
|
1247
|
+
const content = el("div", "lb-bundle-header__content");
|
|
1248
|
+
const title = el("h3", "lb-bundle-title");
|
|
1249
|
+
title.textContent = bundle.title;
|
|
1250
|
+
content.appendChild(title);
|
|
1251
|
+
header.appendChild(content);
|
|
1252
|
+
if (wc.pricing.showSaveBadge) {
|
|
1253
|
+
const badge = badgeFor(resolved[selectedIndex], currency, discountType);
|
|
1254
|
+
if (badge) {
|
|
1255
|
+
const badgeEl = el("span", "lb-bundle-header__badge", {
|
|
1256
|
+
"data-header-badge": ""
|
|
1257
|
+
});
|
|
1258
|
+
badgeEl.textContent = badge;
|
|
1259
|
+
header.appendChild(badgeEl);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return header;
|
|
1263
|
+
}
|
|
1264
|
+
function renderTierCard(r, isSelected, currency, popularLabel, showComparePrice, showPerUnitPrice) {
|
|
1265
|
+
const tier = el("div", "lb-volume__tier", {
|
|
1266
|
+
role: "radio",
|
|
1267
|
+
"aria-checked": String(isSelected),
|
|
1268
|
+
tabindex: isSelected ? "0" : "-1",
|
|
1269
|
+
"data-tier-index": String(r.index),
|
|
1270
|
+
"data-tier-qty": String(r.qty)
|
|
1271
|
+
});
|
|
1272
|
+
const radio = el("span", "lb-volume__radio");
|
|
1273
|
+
radio.appendChild(el("span", "lb-volume__radio-dot"));
|
|
1274
|
+
tier.appendChild(radio);
|
|
1275
|
+
const grid = el("span", "lb-volume__tier-grid");
|
|
1276
|
+
const label = el("span", "lb-volume__tier-label");
|
|
1277
|
+
label.textContent = `Buy ${r.qty}`;
|
|
1278
|
+
grid.appendChild(label);
|
|
1279
|
+
const price = el("span", "lb-volume__tier-price");
|
|
1280
|
+
if (showComparePrice && r.pricePerUnitCents < r.basePricePerUnitCents) {
|
|
1281
|
+
const compare = el("span", "lb-volume__tier-compare");
|
|
1282
|
+
compare.textContent = formatCents(r.basePricePerUnitCents, currency);
|
|
1283
|
+
price.appendChild(compare);
|
|
1284
|
+
}
|
|
1285
|
+
if (showPerUnitPrice) {
|
|
1286
|
+
const each = document.createElement("span");
|
|
1287
|
+
each.setAttribute("data-tier-price-each", "");
|
|
1288
|
+
each.textContent = formatCents(r.pricePerUnitCents, currency);
|
|
1289
|
+
price.appendChild(each);
|
|
1290
|
+
const unit = el("span", "lb-volume__tier-unit");
|
|
1291
|
+
unit.textContent = " each";
|
|
1292
|
+
price.appendChild(unit);
|
|
1293
|
+
}
|
|
1294
|
+
grid.appendChild(price);
|
|
1295
|
+
tier.appendChild(grid);
|
|
1296
|
+
const badge = el("span", "lb-volume__tier-badge");
|
|
1297
|
+
if (popularLabel) {
|
|
1298
|
+
badge.textContent = popularLabel;
|
|
1299
|
+
} else {
|
|
1300
|
+
badge.style.display = "none";
|
|
1301
|
+
}
|
|
1302
|
+
tier.appendChild(badge);
|
|
1303
|
+
return tier;
|
|
1304
|
+
}
|
|
1305
|
+
function renderPricingRow2(resolved, selectedIndex, currency, showItemCount, showCompareAtPrice) {
|
|
1306
|
+
const r = resolved[selectedIndex];
|
|
1307
|
+
const totalCents = r ? r.pricePerUnitCents * r.qty : 0;
|
|
1308
|
+
const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;
|
|
1309
|
+
const savings = Math.max(0, undiscountedCents - totalCents);
|
|
1310
|
+
const row = el("div", "lb-bundle-pricing");
|
|
1311
|
+
const label = el("span", "lb-bundle-pricing__label", {
|
|
1312
|
+
"data-total-label": ""
|
|
76
1313
|
});
|
|
77
|
-
|
|
1314
|
+
label.textContent = "Total";
|
|
1315
|
+
if (showItemCount && r) {
|
|
1316
|
+
const count = document.createElement("span");
|
|
1317
|
+
count.setAttribute("data-item-count", "");
|
|
1318
|
+
count.textContent = ` (${r.qty} item${r.qty === 1 ? "" : "s"})`;
|
|
1319
|
+
label.appendChild(count);
|
|
1320
|
+
}
|
|
1321
|
+
row.appendChild(label);
|
|
1322
|
+
const prices = el("span", "lb-bundle-pricing__prices");
|
|
1323
|
+
if (showCompareAtPrice && savings > 0) {
|
|
1324
|
+
const compare = el("span", "lb-bundle-compare-price", {
|
|
1325
|
+
"data-compare-price": ""
|
|
1326
|
+
});
|
|
1327
|
+
compare.textContent = formatCents(undiscountedCents, currency);
|
|
1328
|
+
prices.appendChild(compare);
|
|
1329
|
+
}
|
|
1330
|
+
const sale = el("span", "lb-bundle-sale-price", { "data-total-price": "" });
|
|
1331
|
+
sale.textContent = formatCents(totalCents, currency);
|
|
1332
|
+
prices.appendChild(sale);
|
|
1333
|
+
row.appendChild(prices);
|
|
1334
|
+
return row;
|
|
1335
|
+
}
|
|
1336
|
+
function renderSavingsBar3(resolved, selectedIndex, currency) {
|
|
1337
|
+
const r = resolved[selectedIndex];
|
|
1338
|
+
const totalCents = r ? r.pricePerUnitCents * r.qty : 0;
|
|
1339
|
+
const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;
|
|
1340
|
+
const savings = Math.max(0, undiscountedCents - totalCents);
|
|
1341
|
+
const bar = el("div", "lb-bundle-savings-bar", { "data-savings-bar": "" });
|
|
1342
|
+
if (savings <= 0) bar.style.display = "none";
|
|
1343
|
+
const labelEl = document.createElement("span");
|
|
1344
|
+
labelEl.textContent = "You save";
|
|
1345
|
+
bar.appendChild(labelEl);
|
|
1346
|
+
const amount = el("span", "", { "data-savings-amount": "" });
|
|
1347
|
+
amount.textContent = formatCents(savings, currency);
|
|
1348
|
+
bar.appendChild(amount);
|
|
1349
|
+
return bar;
|
|
1350
|
+
}
|
|
1351
|
+
function renderCta2(bundle, onClick) {
|
|
1352
|
+
const product = bundle.products[0];
|
|
1353
|
+
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", "");
|
|
1358
|
+
if (!isAvailable) button.disabled = true;
|
|
1359
|
+
button.textContent = isAvailable ? bundle.widgetConfig.cta.ctaText || "Add to cart" : "Sold out";
|
|
1360
|
+
button.addEventListener("click", () => {
|
|
1361
|
+
if (button.disabled) return;
|
|
1362
|
+
onClick();
|
|
1363
|
+
});
|
|
1364
|
+
return button;
|
|
1365
|
+
}
|
|
1366
|
+
function badgeFor(resolved, currency, discountType) {
|
|
1367
|
+
if (!resolved) return null;
|
|
1368
|
+
const { tier } = resolved;
|
|
1369
|
+
if (discountType === "fixed_amount") {
|
|
1370
|
+
const amount = tier.amount ?? 0;
|
|
1371
|
+
if (amount > 0) return `-${formatCents(Math.round(amount * 100), currency)}`;
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
if (discountType === "percentage") {
|
|
1375
|
+
const pct = tier.percentage ?? 0;
|
|
1376
|
+
if (pct > 0) return `-${Math.round(pct)}%`;
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
function pickBestTierIndex(resolved) {
|
|
1382
|
+
let bestSavings = 0;
|
|
1383
|
+
let bestIndex = 0;
|
|
1384
|
+
resolved.forEach((r, i) => {
|
|
1385
|
+
const savings = r.basePricePerUnitCents - r.pricePerUnitCents;
|
|
1386
|
+
if (savings > bestSavings) {
|
|
1387
|
+
bestSavings = savings;
|
|
1388
|
+
bestIndex = i;
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
return bestIndex;
|
|
1392
|
+
}
|
|
1393
|
+
function clamp(n, min, max) {
|
|
1394
|
+
return Math.max(min, Math.min(max, n));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/lime-bundle.ts
|
|
1398
|
+
import { applyWidgetConfigVars } from "@lime-bundles/core";
|
|
1399
|
+
|
|
1400
|
+
// src/styles/bundle-css.ts
|
|
1401
|
+
var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle widget types */
|
|
1402
|
+
|
|
1403
|
+
.lb-bundle-widget.lb-bundle-widget,
|
|
1404
|
+
.lb-bundle-widget.lb-bundle-widget * {
|
|
1405
|
+
line-height: normal;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.lb-bundle-widget {
|
|
1409
|
+
/* Internal CSS-only vars (not merchant-configurable). */
|
|
1410
|
+
--lb-text-muted: #666666;
|
|
1411
|
+
--lb-thumbnail-bg: #F0F0F0;
|
|
1412
|
+
--lb-widget-pad: 20px;
|
|
1413
|
+
--lb-progress-color: var(--lb-primary-color);
|
|
1414
|
+
|
|
1415
|
+
font-family: inherit;
|
|
1416
|
+
font-size: 16px;
|
|
1417
|
+
background: var(--lb-bg);
|
|
1418
|
+
border: var(--lb-border-width) solid var(--lb-border);
|
|
1419
|
+
border-radius: var(--lb-radius);
|
|
1420
|
+
padding: var(--lb-widget-pad) var(--lb-widget-pad) 20px;
|
|
1421
|
+
box-sizing: border-box;
|
|
1422
|
+
/* Cap the widget at a comfortable reading width on desktop. Below
|
|
1423
|
+
440px viewports the container is already narrower than the cap,
|
|
1424
|
+
so the rule is inert on mobile. */
|
|
1425
|
+
max-width: 440px;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/* Countdown timer bar \u2014 sits below the gradient header */
|
|
1429
|
+
.lb-bundle-countdown {
|
|
1430
|
+
margin: 0 calc(-1 * var(--lb-widget-pad)) 20px;
|
|
1431
|
+
padding: 12px 20px;
|
|
1432
|
+
background: var(--lb-countdown-bg);
|
|
1433
|
+
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
1434
|
+
display: flex;
|
|
1435
|
+
align-items: center;
|
|
1436
|
+
justify-content: space-between;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.lb-bundle-countdown__label {
|
|
1440
|
+
display: flex;
|
|
1441
|
+
align-items: center;
|
|
1442
|
+
gap: 8px;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.lb-bundle-countdown__label svg {
|
|
1446
|
+
width: 16px;
|
|
1447
|
+
height: 16px;
|
|
1448
|
+
flex-shrink: 0;
|
|
1449
|
+
color: var(--lb-countdown-text);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.lb-bundle-countdown__label span {
|
|
1453
|
+
font-size: 12px;
|
|
1454
|
+
font-weight: 500;
|
|
1455
|
+
line-height: 1;
|
|
1456
|
+
color: var(--lb-countdown-text);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.lb-bundle-countdown__timer {
|
|
1460
|
+
font-family: 'SF Mono', 'Roboto Mono', ui-monospace, monospace;
|
|
1461
|
+
font-size: 12px;
|
|
1462
|
+
font-weight: 600;
|
|
1463
|
+
line-height: 1;
|
|
1464
|
+
color: var(--lb-countdown-text);
|
|
1465
|
+
letter-spacing: 0.02em;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/* Hide wrapper when the inner snippet rendered nothing (product OOS / unfulfillable) */
|
|
1469
|
+
.lb-bundle-widget:not(:has(.lb-fixed, .lb-mix-match, .lb-volume)) {
|
|
1470
|
+
display: none;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/* Adjacent widget spacing \u2014 separates multiple bundles on the same product page */
|
|
1474
|
+
.lb-bundle-widget + .lb-bundle-widget {
|
|
1475
|
+
margin-top: 24px;
|
|
1476
|
+
padding-top: 24px;
|
|
1477
|
+
border-top: 1px solid var(--lb-border);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/* Header \u2014 gradient banner with title + savings badge */
|
|
1481
|
+
.lb-bundle-header {
|
|
1482
|
+
margin: calc(-1 * var(--lb-widget-pad)) calc(-1 * var(--lb-widget-pad)) 20px;
|
|
1483
|
+
padding: 20px 20px;
|
|
1484
|
+
background: var(--lb-header-bg);
|
|
1485
|
+
/* Match the widget's inner border curve so there's no background gap at the top corners. */
|
|
1486
|
+
border-radius: max(0px, calc(var(--lb-radius) - var(--lb-border-width))) max(0px, calc(var(--lb-radius) - var(--lb-border-width))) 0 0;
|
|
1487
|
+
display: flex;
|
|
1488
|
+
align-items: center;
|
|
1489
|
+
justify-content: space-between;
|
|
1490
|
+
gap: 16px;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/* When countdown follows header, remove header bottom margin */
|
|
1494
|
+
.lb-bundle-header:has(+ .lb-bundle-countdown) {
|
|
1495
|
+
margin-bottom: 0;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.lb-bundle-header__content {
|
|
1499
|
+
flex: 1;
|
|
1500
|
+
min-width: 0;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
.lb-bundle-title {
|
|
1504
|
+
font-size: 20px;
|
|
1505
|
+
font-weight: 700;
|
|
1506
|
+
line-height: 28px;
|
|
1507
|
+
letter-spacing: -0.02em;
|
|
1508
|
+
color: var(--lb-text);
|
|
1509
|
+
margin: 0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
.lb-bundle-header .lb-bundle-title {
|
|
1513
|
+
color: var(--lb-header-text);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.lb-bundle-subtitle {
|
|
1517
|
+
font-size: 16px;
|
|
1518
|
+
font-weight: 400;
|
|
1519
|
+
line-height: 20px;
|
|
1520
|
+
color: var(--lb-text-muted);
|
|
1521
|
+
margin: 4px 0 0;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.lb-bundle-header .lb-bundle-subtitle {
|
|
1525
|
+
color: var(--lb-header-text);
|
|
1526
|
+
opacity: 0.85;
|
|
1527
|
+
margin-top: 8px;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.lb-bundle-header:has(.lb-bundle-subtitle) {
|
|
1531
|
+
align-items: flex-start;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
.lb-bundle-header__badge {
|
|
1535
|
+
background: var(--lb-save-badge-bg);
|
|
1536
|
+
color: var(--lb-save-badge-text);
|
|
1537
|
+
border: var(--lb-save-badge-border-width) solid var(--lb-save-badge-border-color);
|
|
1538
|
+
font-size: 16px;
|
|
1539
|
+
font-weight: 700;
|
|
1540
|
+
line-height: 1;
|
|
1541
|
+
padding: 4px 12px;
|
|
1542
|
+
border-radius: var(--lb-save-badge-radius);
|
|
1543
|
+
white-space: nowrap;
|
|
1544
|
+
flex-shrink: 0;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/* Override Dawn's \`div:empty { display: none }\` reset for decorative elements */
|
|
1548
|
+
.lb-bundle-divider:empty,
|
|
1549
|
+
.lb-mix-match__progress-fill:empty {
|
|
1550
|
+
display: block;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/* Divider */
|
|
1554
|
+
.lb-bundle-divider {
|
|
1555
|
+
height: 1px;
|
|
1556
|
+
background: color-mix(in srgb, var(--lb-text) 7%, transparent);
|
|
1557
|
+
margin: 16px 0;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/* Product rows */
|
|
1561
|
+
.lb-bundle-product-row {
|
|
1562
|
+
display: flex;
|
|
1563
|
+
gap: 12px;
|
|
1564
|
+
padding: 12px 0;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.lb-bundle-thumbnail {
|
|
1568
|
+
position: relative;
|
|
1569
|
+
width: 48px;
|
|
1570
|
+
height: 48px;
|
|
1571
|
+
min-width: 48px;
|
|
1572
|
+
background: var(--lb-thumbnail-bg);
|
|
1573
|
+
border-radius: 8px;
|
|
1574
|
+
overflow: hidden;
|
|
1575
|
+
display: flex;
|
|
1576
|
+
align-items: center;
|
|
1577
|
+
justify-content: center;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.lb-bundle-thumbnail img {
|
|
1581
|
+
width: 100%;
|
|
1582
|
+
height: 100%;
|
|
1583
|
+
object-fit: cover;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.lb-bundle-thumbnail svg {
|
|
1587
|
+
width: 28px;
|
|
1588
|
+
height: 28px;
|
|
1589
|
+
color: #BBBBBB;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
.lb-bundle-product-info {
|
|
1593
|
+
flex: 1;
|
|
1594
|
+
min-width: 0;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.lb-bundle-product-name {
|
|
1598
|
+
font-size: 16px;
|
|
1599
|
+
font-weight: 700;
|
|
1600
|
+
line-height: 20px;
|
|
1601
|
+
color: var(--lb-text);
|
|
1602
|
+
margin: 0;
|
|
1603
|
+
text-decoration: none;
|
|
1604
|
+
display: block;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.lb-bundle-product-name:hover {
|
|
1608
|
+
text-decoration: underline;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.lb-bundle-product-price {
|
|
1612
|
+
/* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
1613
|
+
display: var(--lb-product-price-display, block);
|
|
1614
|
+
font-size: 12px;
|
|
1615
|
+
font-weight: 400;
|
|
1616
|
+
line-height: 20px;
|
|
1617
|
+
color: var(--lb-text);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
.lb-bundle-product-prices {
|
|
1621
|
+
display: flex;
|
|
1622
|
+
align-items: baseline;
|
|
1623
|
+
gap: 6px;
|
|
1624
|
+
flex-wrap: wrap;
|
|
1625
|
+
margin-top: 2px;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.lb-bundle-product-compare-price {
|
|
1629
|
+
/* --lb-product-compare-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
1630
|
+
display: var(--lb-product-compare-display, inline);
|
|
1631
|
+
font-size: 12px;
|
|
1632
|
+
font-weight: 400;
|
|
1633
|
+
line-height: 20px;
|
|
1634
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
1635
|
+
text-decoration: line-through;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/* Setting \`display\` above outranks the UA [hidden] rule \u2014 restore it so rows
|
|
1639
|
+
without a compare-at price don't leave a phantom flex item + gap. */
|
|
1640
|
+
.lb-bundle-product-compare-price[hidden] {
|
|
1641
|
+
display: none;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.lb-bundle-variant-badge {
|
|
1645
|
+
display: inline-block;
|
|
1646
|
+
border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
|
|
1647
|
+
border-radius: var(--lb-variant-radius);
|
|
1648
|
+
padding: 8px 12px;
|
|
1649
|
+
font-size: 12px;
|
|
1650
|
+
line-height: 16px;
|
|
1651
|
+
color: var(--lb-text-muted);
|
|
1652
|
+
margin-top: 8px;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
.lb-bundle-quantity {
|
|
1656
|
+
font-size: 12px;
|
|
1657
|
+
line-height: 16px;
|
|
1658
|
+
color: var(--lb-text-muted);
|
|
1659
|
+
margin-left: 8px;
|
|
1660
|
+
white-space: nowrap;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/* Pricing row \u2014 label left, prices right */
|
|
1664
|
+
.lb-bundle-pricing {
|
|
1665
|
+
display: flex;
|
|
1666
|
+
align-items: baseline;
|
|
1667
|
+
justify-content: space-between;
|
|
1668
|
+
padding: 4px 0 8px;
|
|
1669
|
+
gap: 12px;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.lb-bundle-pricing__label {
|
|
1673
|
+
font-size: 16px;
|
|
1674
|
+
font-weight: 400;
|
|
1675
|
+
line-height: 20px;
|
|
1676
|
+
color: var(--lb-text);
|
|
1677
|
+
white-space: nowrap;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
.lb-bundle-pricing__prices {
|
|
1681
|
+
display: flex;
|
|
1682
|
+
align-items: baseline;
|
|
1683
|
+
gap: 8px;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.lb-bundle-sale-price {
|
|
1687
|
+
font-size: 20px;
|
|
1688
|
+
font-weight: 700;
|
|
1689
|
+
line-height: 1;
|
|
1690
|
+
letter-spacing: -0.02em;
|
|
1691
|
+
color: var(--lb-text);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.lb-bundle-compare-price {
|
|
1695
|
+
font-size: 16px;
|
|
1696
|
+
font-weight: 400;
|
|
1697
|
+
line-height: 20px;
|
|
1698
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
1699
|
+
text-decoration: line-through;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/* Savings bar \u2014 green banner below pricing */
|
|
1703
|
+
.lb-bundle-savings-bar {
|
|
1704
|
+
display: flex;
|
|
1705
|
+
align-items: center;
|
|
1706
|
+
justify-content: space-between;
|
|
1707
|
+
background: var(--lb-savings-bar-bg);
|
|
1708
|
+
color: var(--lb-savings-bar-text);
|
|
1709
|
+
border: var(--lb-savings-bar-border-width) solid var(--lb-savings-bar-border-color);
|
|
1710
|
+
font-size: 16px;
|
|
1711
|
+
font-weight: 600;
|
|
1712
|
+
line-height: 20px;
|
|
1713
|
+
padding: 12px 16px;
|
|
1714
|
+
border-radius: var(--lb-savings-bar-radius);
|
|
1715
|
+
margin-bottom: 12px;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/* Quantity badge \u2014 overlay on thumbnail top-right */
|
|
1719
|
+
.lb-bundle-qty-badge.lb-bundle-qty-badge {
|
|
1720
|
+
position: absolute;
|
|
1721
|
+
top: -8px;
|
|
1722
|
+
right: -8px;
|
|
1723
|
+
/* --lb-qty-badge-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
1724
|
+
display: var(--lb-qty-badge-display, flex);
|
|
1725
|
+
align-items: center;
|
|
1726
|
+
justify-content: center;
|
|
1727
|
+
width: 24px;
|
|
1728
|
+
height: 24px;
|
|
1729
|
+
border-radius: 50%;
|
|
1730
|
+
background: var(--lb-qty-badge-bg);
|
|
1731
|
+
border: var(--lb-image-border-width) solid var(--lb-image-border-color);
|
|
1732
|
+
color: var(--lb-qty-badge-color);
|
|
1733
|
+
font-size: 12px;
|
|
1734
|
+
font-weight: 700;
|
|
1735
|
+
line-height: 0;
|
|
1736
|
+
text-align: center;
|
|
1737
|
+
z-index: 1;
|
|
1738
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/* Override Dawn's \`div:empty\` for savings bar when hidden */
|
|
1742
|
+
.lb-bundle-savings-bar:empty {
|
|
1743
|
+
display: none;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/* CTA button */
|
|
1747
|
+
.lb-bundle-cta {
|
|
1748
|
+
display: block;
|
|
1749
|
+
width: 100%;
|
|
1750
|
+
padding: 16px;
|
|
1751
|
+
border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);
|
|
1752
|
+
border-radius: var(--lb-cta-radius);
|
|
1753
|
+
font-size: 16px;
|
|
1754
|
+
font-weight: 600;
|
|
1755
|
+
line-height: 20px;
|
|
1756
|
+
cursor: pointer;
|
|
1757
|
+
text-align: center;
|
|
1758
|
+
transition: opacity 0.15s ease;
|
|
1759
|
+
font-family: inherit;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.lb-bundle-cta:not(:disabled) {
|
|
1763
|
+
background: var(--lb-primary-color);
|
|
1764
|
+
color: var(--lb-btn-text);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
.lb-bundle-cta:not(:disabled):hover {
|
|
1768
|
+
opacity: 0.9;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.lb-bundle-cta:disabled {
|
|
1772
|
+
background: color-mix(in srgb, var(--lb-primary-color) 35%, var(--lb-bg));
|
|
1773
|
+
color: color-mix(in srgb, var(--lb-btn-text) 85%, transparent);
|
|
1774
|
+
cursor: not-allowed;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.lb-bundle-cta[data-loading="true"] {
|
|
1778
|
+
opacity: 0.7;
|
|
1779
|
+
cursor: wait;
|
|
1780
|
+
pointer-events: none;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/* Error message */
|
|
1784
|
+
.lb-bundle-error {
|
|
1785
|
+
font-size: 16px;
|
|
1786
|
+
color: #D72C0D;
|
|
1787
|
+
margin-top: 8px;
|
|
1788
|
+
display: none;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.lb-bundle-error[data-visible="true"] {
|
|
1792
|
+
display: block;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/* Visually hidden \u2014 accessible to screen readers only */
|
|
1796
|
+
.lb-visually-hidden {
|
|
1797
|
+
position: absolute !important;
|
|
1798
|
+
width: 1px !important;
|
|
1799
|
+
height: 1px !important;
|
|
1800
|
+
padding: 0 !important;
|
|
1801
|
+
margin: -1px !important;
|
|
1802
|
+
overflow: hidden !important;
|
|
1803
|
+
clip-path: inset(50%) !important;
|
|
1804
|
+
white-space: nowrap !important;
|
|
1805
|
+
border: 0 !important;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/* Placeholder SVG icon for missing images */
|
|
1809
|
+
.lb-bundle-placeholder-icon {
|
|
1810
|
+
width: 28px;
|
|
1811
|
+
height: 28px;
|
|
1812
|
+
stroke: #BBBBBB;
|
|
1813
|
+
stroke-width: 1.5;
|
|
1814
|
+
fill: none;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/* Out-of-stock product row */
|
|
1818
|
+
.lb-bundle-product-row--oos {
|
|
1819
|
+
opacity: 0.5;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
.lb-bundle-oos-label {
|
|
1823
|
+
font-size: 12px;
|
|
1824
|
+
font-weight: 500;
|
|
1825
|
+
color: #D72C0D;
|
|
1826
|
+
white-space: nowrap;
|
|
1827
|
+
margin-left: auto;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */
|
|
1831
|
+
.lb-ab-pending {
|
|
1832
|
+
visibility: hidden;
|
|
1833
|
+
}
|
|
1834
|
+
`;
|
|
1835
|
+
var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
|
|
1836
|
+
|
|
1837
|
+
.lb-fixed__products {
|
|
1838
|
+
display: flex;
|
|
1839
|
+
flex-direction: column;
|
|
1840
|
+
gap: 0;
|
|
1841
|
+
margin: 0;
|
|
78
1842
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1843
|
+
|
|
1844
|
+
/* Fixed bundles: product rows */
|
|
1845
|
+
.lb-fixed .lb-bundle-product-row {
|
|
1846
|
+
gap: 16px;
|
|
1847
|
+
align-items: center;
|
|
83
1848
|
}
|
|
84
1849
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1850
|
+
/* Fixed bundles: larger thumbnails */
|
|
1851
|
+
.lb-fixed .lb-bundle-thumbnail {
|
|
1852
|
+
width: 60px;
|
|
1853
|
+
height: 60px;
|
|
1854
|
+
min-width: 60px;
|
|
1855
|
+
border-radius: var(--lb-image-border-radius);
|
|
1856
|
+
border: var(--lb-image-border-width) solid var(--lb-image-border-color);
|
|
1857
|
+
box-sizing: border-box;
|
|
1858
|
+
overflow: visible;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
.lb-fixed .lb-bundle-thumbnail img {
|
|
1862
|
+
border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));
|
|
1863
|
+
border: none;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/* Variant picker select \u2014 styled to match the variant badge aesthetic */
|
|
1867
|
+
.lb-bundle-variant-select {
|
|
1868
|
+
margin-top: 8px;
|
|
1869
|
+
display: inline-block;
|
|
1870
|
+
border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
|
|
1871
|
+
border-radius: var(--lb-variant-radius);
|
|
1872
|
+
padding: 8px 32px 8px 12px;
|
|
1873
|
+
font-size: 12px;
|
|
1874
|
+
line-height: 16px;
|
|
1875
|
+
color: var(--lb-text);
|
|
1876
|
+
background: var(--lb-bg);
|
|
1877
|
+
font-family: inherit;
|
|
1878
|
+
cursor: pointer;
|
|
1879
|
+
appearance: none;
|
|
1880
|
+
-webkit-appearance: none;
|
|
1881
|
+
background-image: var(--lb-variant-chevron);
|
|
1882
|
+
background-repeat: no-repeat;
|
|
1883
|
+
background-position: right 8px center;
|
|
1884
|
+
background-size: 12px;
|
|
1885
|
+
max-width: 100%;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
.lb-bundle-variant-select:focus-visible {
|
|
1889
|
+
outline: 2px solid var(--lb-primary-color);
|
|
1890
|
+
outline-offset: 2px;
|
|
1891
|
+
}
|
|
1892
|
+
`;
|
|
1893
|
+
var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
1894
|
+
|
|
1895
|
+
/* === Progress Bar === */
|
|
1896
|
+
.lb-mix-match__progress {
|
|
1897
|
+
margin-bottom: 16px;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
.lb-mix-match__progress-labels {
|
|
1901
|
+
display: flex;
|
|
1902
|
+
justify-content: space-between;
|
|
1903
|
+
margin-bottom: 8px;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.lb-mix-match__progress-count {
|
|
1907
|
+
font-size: 12px;
|
|
1908
|
+
font-weight: 500;
|
|
1909
|
+
line-height: 16px;
|
|
1910
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
.lb-mix-match__progress-remaining {
|
|
1914
|
+
font-size: 12px;
|
|
1915
|
+
font-weight: 500;
|
|
1916
|
+
line-height: 16px;
|
|
1917
|
+
color: var(--lb-text);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
.lb-mix-match__progress-track {
|
|
1921
|
+
width: 100%;
|
|
1922
|
+
height: 4px;
|
|
1923
|
+
background: color-mix(in srgb, var(--lb-text) 10%, transparent);
|
|
1924
|
+
border-radius: 4px;
|
|
1925
|
+
overflow: hidden;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.lb-mix-match__progress-fill {
|
|
1929
|
+
height: 100%;
|
|
1930
|
+
background: var(--lb-text);
|
|
1931
|
+
border-radius: 4px;
|
|
1932
|
+
transition: width 0.3s ease;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/* === Slots === */
|
|
1936
|
+
.lb-mix-match__slot {
|
|
1937
|
+
cursor: pointer;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
.lb-mix-match .lb-bundle-product-row {
|
|
1941
|
+
align-items: center;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
.lb-mix-match__slot--empty:focus-visible {
|
|
1945
|
+
outline: 2px solid var(--lb-primary-color);
|
|
1946
|
+
outline-offset: 2px;
|
|
1947
|
+
border-radius: 8px;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
.lb-mix-match__slot--empty .lb-mix-match__empty-thumb {
|
|
1951
|
+
width: 60px;
|
|
1952
|
+
height: 60px;
|
|
1953
|
+
min-width: 60px;
|
|
1954
|
+
border: var(--lb-image-border-width) dashed color-mix(in srgb, var(--lb-text) 20%, transparent);
|
|
1955
|
+
border-radius: var(--lb-image-border-radius);
|
|
1956
|
+
box-sizing: border-box;
|
|
1957
|
+
display: flex;
|
|
1958
|
+
align-items: center;
|
|
1959
|
+
justify-content: center;
|
|
1960
|
+
color: color-mix(in srgb, var(--lb-text) 35%, transparent);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.lb-mix-match__empty-text {
|
|
1964
|
+
font-size: 16px;
|
|
1965
|
+
line-height: 20px;
|
|
1966
|
+
color: color-mix(in srgb, var(--lb-text) 45%, transparent);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/* Filled slot */
|
|
1970
|
+
.lb-mix-match__slot--filled {
|
|
1971
|
+
cursor: default;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
.lb-mix-match .lb-bundle-thumbnail {
|
|
1975
|
+
width: 60px;
|
|
1976
|
+
height: 60px;
|
|
1977
|
+
min-width: 60px;
|
|
1978
|
+
border-radius: var(--lb-image-border-radius);
|
|
1979
|
+
border: var(--lb-image-border-width) solid var(--lb-image-border-color);
|
|
1980
|
+
box-sizing: border-box;
|
|
1981
|
+
overflow: visible;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.lb-mix-match .lb-bundle-thumbnail img {
|
|
1985
|
+
border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));
|
|
1986
|
+
border: none;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
.lb-mix-match__slot--filled .lb-mix-match__filled-info {
|
|
1990
|
+
flex: 1;
|
|
1991
|
+
min-width: 0;
|
|
1992
|
+
display: flex;
|
|
1993
|
+
flex-direction: column;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
.lb-mix-match__slot--filled .lb-mix-match__filled-title {
|
|
1997
|
+
font-size: 16px;
|
|
1998
|
+
font-weight: 700;
|
|
1999
|
+
line-height: 20px;
|
|
2000
|
+
color: var(--lb-text);
|
|
2001
|
+
text-decoration: none;
|
|
2002
|
+
overflow-wrap: break-word;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
.lb-mix-match__slot--filled a.lb-mix-match__filled-title:hover {
|
|
2006
|
+
text-decoration: underline;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
.lb-mix-match__slot--filled .lb-mix-match__filled-variant {
|
|
2010
|
+
font-size: 12px;
|
|
2011
|
+
line-height: 20px;
|
|
2012
|
+
margin-top: 2px;
|
|
2013
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
.lb-mix-match__filled-price {
|
|
2017
|
+
/* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
2018
|
+
display: var(--lb-product-price-display, block);
|
|
2019
|
+
font-size: 12px;
|
|
2020
|
+
line-height: 20px;
|
|
2021
|
+
margin-top: 4px;
|
|
2022
|
+
color: var(--lb-text);
|
|
2023
|
+
font-weight: 500;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
.lb-mix-match__filled-compare {
|
|
2027
|
+
/* --lb-product-compare-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
2028
|
+
display: var(--lb-product-compare-display, inline);
|
|
2029
|
+
font-size: 12px;
|
|
2030
|
+
font-weight: 400;
|
|
2031
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
2032
|
+
text-decoration: line-through;
|
|
2033
|
+
margin-right: 4px;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
.lb-mix-match__slot-remove {
|
|
2037
|
+
min-width: 44px;
|
|
2038
|
+
min-height: 44px;
|
|
2039
|
+
display: flex;
|
|
2040
|
+
align-items: center;
|
|
2041
|
+
justify-content: center;
|
|
2042
|
+
background: none;
|
|
2043
|
+
border: none;
|
|
2044
|
+
cursor: pointer;
|
|
2045
|
+
color: var(--lb-text);
|
|
2046
|
+
padding: 0;
|
|
2047
|
+
margin-left: auto;
|
|
2048
|
+
border-radius: 8px;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
.lb-mix-match__slot-remove:hover {
|
|
2052
|
+
color: var(--lb-text);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.lb-mix-match__slot-remove:focus-visible {
|
|
2056
|
+
outline: 2px solid var(--lb-primary-color);
|
|
2057
|
+
outline-offset: 2px;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/* Price placeholder */
|
|
2061
|
+
.lb-mix-match__price-placeholder {
|
|
2062
|
+
padding: 4px 0 16px;
|
|
2063
|
+
text-align: center;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
.lb-mix-match__price-placeholder-text {
|
|
2067
|
+
font-size: 16px;
|
|
2068
|
+
color: var(--lb-text);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/* === Modal Overlay === */
|
|
2072
|
+
.lb-mix-match__modal-overlay {
|
|
2073
|
+
position: fixed;
|
|
2074
|
+
top: 0;
|
|
2075
|
+
left: 0;
|
|
2076
|
+
right: 0;
|
|
2077
|
+
bottom: 0;
|
|
2078
|
+
background: rgba(0, 0, 0, 0.5);
|
|
2079
|
+
z-index: 9999;
|
|
2080
|
+
display: flex;
|
|
2081
|
+
align-items: center;
|
|
2082
|
+
justify-content: center;
|
|
2083
|
+
opacity: 0;
|
|
2084
|
+
transition: opacity 0.2s ease-out;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
.lb-mix-match__modal-overlay--open {
|
|
2088
|
+
opacity: 1;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
/* === Modal Panel === */
|
|
2092
|
+
.lb-mix-match__modal {
|
|
2093
|
+
background: var(--lb-picker-bg);
|
|
2094
|
+
color: var(--lb-picker-text);
|
|
2095
|
+
border-radius: var(--lb-picker-radius);
|
|
2096
|
+
border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
|
|
2097
|
+
box-sizing: border-box;
|
|
2098
|
+
width: 100%;
|
|
2099
|
+
max-width: 480px;
|
|
2100
|
+
max-height: 70vh;
|
|
2101
|
+
display: flex;
|
|
2102
|
+
flex-direction: column;
|
|
2103
|
+
overflow: hidden;
|
|
2104
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);
|
|
2105
|
+
transform: translateY(24px);
|
|
2106
|
+
transition: transform 0.25s ease-out;
|
|
2107
|
+
will-change: transform;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.lb-mix-match__modal-overlay--open .lb-mix-match__modal {
|
|
2111
|
+
transform: translateY(0);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
/* === Modal Header === */
|
|
2115
|
+
.lb-mix-match__modal-header {
|
|
2116
|
+
display: flex;
|
|
2117
|
+
align-items: center;
|
|
2118
|
+
justify-content: space-between;
|
|
2119
|
+
padding: 20px 20px 12px;
|
|
2120
|
+
flex-shrink: 0;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
.lb-mix-match__modal-title {
|
|
2124
|
+
font-size: 20px;
|
|
2125
|
+
font-weight: 600;
|
|
2126
|
+
line-height: 24px;
|
|
2127
|
+
margin: 0;
|
|
2128
|
+
color: inherit;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
.lb-mix-match__modal-close {
|
|
2132
|
+
min-width: 44px;
|
|
2133
|
+
min-height: 44px;
|
|
2134
|
+
display: flex;
|
|
2135
|
+
align-items: center;
|
|
2136
|
+
justify-content: center;
|
|
2137
|
+
background: none;
|
|
2138
|
+
border: none;
|
|
2139
|
+
cursor: pointer;
|
|
2140
|
+
color: var(--lb-picker-text);
|
|
2141
|
+
border-radius: 8px;
|
|
2142
|
+
padding: 0;
|
|
2143
|
+
margin: -12px -12px -12px 0;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
.lb-mix-match__modal-close:focus-visible {
|
|
2147
|
+
outline: 2px solid var(--lb-primary-color);
|
|
2148
|
+
outline-offset: 2px;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/* === Modal Search === */
|
|
2152
|
+
.lb-mix-match__modal-search {
|
|
2153
|
+
padding: 0 20px 12px;
|
|
2154
|
+
position: relative;
|
|
2155
|
+
flex-shrink: 0;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
.lb-mix-match__modal-search-input {
|
|
2159
|
+
width: 100%;
|
|
2160
|
+
padding: 12px 40px 12px 16px;
|
|
2161
|
+
border: var(--lb-picker-search-border-width) solid var(--lb-picker-search-border-color);
|
|
2162
|
+
border-radius: var(--lb-picker-search-radius);
|
|
2163
|
+
font-size: 16px;
|
|
2164
|
+
line-height: 20px;
|
|
2165
|
+
color: var(--lb-picker-text);
|
|
2166
|
+
background: var(--lb-picker-bg);
|
|
2167
|
+
box-sizing: border-box;
|
|
2168
|
+
-webkit-appearance: none;
|
|
2169
|
+
appearance: none;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
.lb-mix-match__modal-search-input::placeholder {
|
|
2173
|
+
color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.lb-mix-match__modal-search-input:focus {
|
|
2177
|
+
outline: none;
|
|
2178
|
+
box-shadow: 0 0 0 1px var(--lb-primary-color);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.lb-mix-match__modal-search-clear {
|
|
2182
|
+
position: absolute;
|
|
2183
|
+
right: 32px;
|
|
2184
|
+
/* Anchor to the input area only \u2014 parent has padding-bottom: 12px which would
|
|
2185
|
+
otherwise push a top:50% center down by 6px. */
|
|
2186
|
+
top: 0;
|
|
2187
|
+
bottom: 12px;
|
|
2188
|
+
min-width: 32px;
|
|
2189
|
+
min-height: 32px;
|
|
2190
|
+
display: flex;
|
|
2191
|
+
align-items: center;
|
|
2192
|
+
justify-content: center;
|
|
2193
|
+
background: none;
|
|
2194
|
+
border: none;
|
|
2195
|
+
cursor: pointer;
|
|
2196
|
+
color: var(--lb-picker-text);
|
|
2197
|
+
padding: 0;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/* === Modal Product List === */
|
|
2201
|
+
.lb-mix-match__modal-list {
|
|
2202
|
+
overflow-y: auto;
|
|
2203
|
+
flex: 1;
|
|
2204
|
+
padding: 0 20px;
|
|
2205
|
+
-webkit-overflow-scrolling: touch;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
.lb-mix-match__modal-product {
|
|
2209
|
+
display: flex;
|
|
2210
|
+
align-items: center;
|
|
2211
|
+
gap: 12px;
|
|
2212
|
+
padding: 12px 0;
|
|
2213
|
+
border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 7%, transparent);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
.lb-mix-match__modal-product:last-child {
|
|
2217
|
+
border-bottom: none;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
.lb-mix-match__modal-product-thumb {
|
|
2221
|
+
position: relative;
|
|
2222
|
+
width: 48px;
|
|
2223
|
+
height: 48px;
|
|
2224
|
+
min-width: 48px;
|
|
2225
|
+
border-radius: var(--lb-picker-product-radius);
|
|
2226
|
+
border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);
|
|
2227
|
+
box-sizing: border-box;
|
|
2228
|
+
overflow: visible;
|
|
2229
|
+
background: #F0F0F0;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
/* Qty badge inside the picker modal inherits picker-product border (width + color) plus
|
|
2233
|
+
inverted picker bg/text for clear contrast against the modal \u2014 always stays round
|
|
2234
|
+
(the badge shape is independent of the thumbnail shape). */
|
|
2235
|
+
.lb-mix-match__modal-product-thumb .lb-bundle-qty-badge.lb-bundle-qty-badge {
|
|
2236
|
+
/* --lb-picker-qty-badge-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
|
|
2237
|
+
display: var(--lb-picker-qty-badge-display, flex);
|
|
2238
|
+
background: var(--lb-picker-qty-badge-bg);
|
|
2239
|
+
color: var(--lb-picker-qty-badge-color);
|
|
2240
|
+
border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
.lb-mix-match__modal-product-thumb img {
|
|
2244
|
+
width: 100%;
|
|
2245
|
+
height: 100%;
|
|
2246
|
+
object-fit: cover;
|
|
2247
|
+
border-radius: max(0px, calc(var(--lb-picker-product-radius) - var(--lb-picker-product-border-width)));
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
.lb-mix-match__modal-product-info {
|
|
2251
|
+
flex: 1;
|
|
2252
|
+
min-width: 0;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
.lb-mix-match__modal-product-title {
|
|
2256
|
+
font-size: 16px;
|
|
2257
|
+
font-weight: 700;
|
|
2258
|
+
line-height: 20px;
|
|
2259
|
+
color: inherit;
|
|
2260
|
+
margin: 0;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
.lb-mix-match__modal-product-price {
|
|
2264
|
+
font-size: 12px;
|
|
2265
|
+
line-height: 20px;
|
|
2266
|
+
color: inherit;
|
|
2267
|
+
margin: 4px 0 0;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
.lb-mix-match__variant-select {
|
|
2271
|
+
font-size: 12px;
|
|
2272
|
+
margin: 4px 0 0;
|
|
2273
|
+
padding: 4px 24px 4px 8px;
|
|
2274
|
+
border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
|
|
2275
|
+
border-radius: var(--lb-picker-variant-radius);
|
|
2276
|
+
background-color: var(--lb-picker-bg);
|
|
2277
|
+
background-image: var(--lb-picker-variant-chevron);
|
|
2278
|
+
background-repeat: no-repeat;
|
|
2279
|
+
background-position: right 6px center;
|
|
2280
|
+
background-size: 12px;
|
|
2281
|
+
color: var(--lb-picker-text);
|
|
2282
|
+
font-family: inherit;
|
|
2283
|
+
min-height: 32px;
|
|
2284
|
+
cursor: pointer;
|
|
2285
|
+
max-width: 120px;
|
|
2286
|
+
appearance: none;
|
|
2287
|
+
-webkit-appearance: none;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
.lb-mix-match__variant-select:focus-visible {
|
|
2291
|
+
outline: 2px solid var(--lb-primary-color);
|
|
2292
|
+
outline-offset: 2px;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
.lb-mix-match__modal-add {
|
|
2296
|
+
padding: 8px 20px;
|
|
2297
|
+
background: var(--lb-picker-add-bg);
|
|
2298
|
+
color: var(--lb-picker-add-label);
|
|
2299
|
+
border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);
|
|
2300
|
+
border-radius: var(--lb-picker-add-radius);
|
|
2301
|
+
box-sizing: border-box;
|
|
2302
|
+
font-size: 12px;
|
|
2303
|
+
font-weight: 600;
|
|
2304
|
+
cursor: pointer;
|
|
2305
|
+
white-space: nowrap;
|
|
2306
|
+
flex-shrink: 0;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
.lb-mix-match__modal-add:hover {
|
|
2310
|
+
opacity: 0.9;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
.lb-mix-match__modal-add:focus-visible {
|
|
2314
|
+
outline: 2px solid var(--lb-primary-color);
|
|
2315
|
+
outline-offset: 2px;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
.lb-mix-match__modal-add:disabled {
|
|
2319
|
+
background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));
|
|
2320
|
+
color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);
|
|
2321
|
+
cursor: not-allowed;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/* Sold out product row */
|
|
2325
|
+
.lb-mix-match__modal-product--sold-out {
|
|
2326
|
+
opacity: 0.5;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.lb-mix-match__modal-product--sold-out .lb-mix-match__modal-sold-out-label {
|
|
2330
|
+
font-size: 12px;
|
|
2331
|
+
color: inherit;
|
|
2332
|
+
font-weight: 500;
|
|
2333
|
+
white-space: nowrap;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
/* === Modal Empty State === */
|
|
2337
|
+
.lb-mix-match__modal-empty {
|
|
2338
|
+
padding: 32px 20px;
|
|
2339
|
+
text-align: center;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
.lb-mix-match__modal-empty p {
|
|
2343
|
+
margin: 0;
|
|
2344
|
+
font-size: 16px;
|
|
2345
|
+
color: var(--lb-picker-text);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/* Hidden utility for search filtering */
|
|
2349
|
+
.lb-hidden {
|
|
2350
|
+
display: none !important;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/* === Mobile Full-Screen Modal === */
|
|
2354
|
+
@media (max-width: 767px) {
|
|
2355
|
+
.lb-mix-match__modal-overlay {
|
|
2356
|
+
align-items: flex-end;
|
|
143
2357
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const total = Array.from(selections.values()).reduce(
|
|
155
|
-
(s, v) => s + v.quantity,
|
|
156
|
-
0
|
|
157
|
-
);
|
|
158
|
-
const validation = validateQuantity(
|
|
159
|
-
total,
|
|
160
|
-
bundle.minQuantity,
|
|
161
|
-
bundle.maxQuantity
|
|
162
|
-
);
|
|
163
|
-
button.disabled = !validation.valid;
|
|
164
|
-
button.textContent = bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;
|
|
165
|
-
validationEl.textContent = validation.message ?? "";
|
|
2358
|
+
|
|
2359
|
+
.lb-mix-match__modal {
|
|
2360
|
+
max-width: 100%;
|
|
2361
|
+
max-height: 90vh;
|
|
2362
|
+
border-radius: 16px 16px 0 0;
|
|
2363
|
+
transform: translateY(100%);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
.lb-mix-match__modal-overlay--open .lb-mix-match__modal {
|
|
2367
|
+
transform: translateY(0);
|
|
166
2368
|
}
|
|
167
|
-
updateCta();
|
|
168
|
-
button.addEventListener("click", () => {
|
|
169
|
-
const lines = Array.from(selections.values()).map((s) => ({
|
|
170
|
-
merchandiseId: s.variantId,
|
|
171
|
-
quantity: s.quantity,
|
|
172
|
-
attributes: [
|
|
173
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
174
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
175
|
-
]
|
|
176
|
-
}));
|
|
177
|
-
if (lines.length === 0) return;
|
|
178
|
-
onAddToCart(lines);
|
|
179
|
-
});
|
|
180
2369
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
2370
|
+
|
|
2371
|
+
/* === Reduced Motion === */
|
|
2372
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2373
|
+
.lb-mix-match__modal-overlay,
|
|
2374
|
+
.lb-mix-match__modal,
|
|
2375
|
+
.lb-mix-match__progress-fill {
|
|
2376
|
+
transition: none;
|
|
2377
|
+
}
|
|
185
2378
|
}
|
|
2379
|
+
`;
|
|
2380
|
+
var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles */
|
|
186
2381
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
qtyWrapper.appendChild(label);
|
|
231
|
-
const qtyControl = document.createElement("div");
|
|
232
|
-
qtyControl.className = "lb-bundle__quantity-control";
|
|
233
|
-
const minusBtn = document.createElement("button");
|
|
234
|
-
minusBtn.textContent = "\u2212";
|
|
235
|
-
minusBtn.setAttribute("aria-label", "Decrease quantity");
|
|
236
|
-
const qtyInput = document.createElement("input");
|
|
237
|
-
qtyInput.type = "number";
|
|
238
|
-
qtyInput.min = "1";
|
|
239
|
-
qtyInput.value = "1";
|
|
240
|
-
qtyInput.className = "lb-bundle__quantity-input";
|
|
241
|
-
const plusBtn = document.createElement("button");
|
|
242
|
-
plusBtn.textContent = "+";
|
|
243
|
-
plusBtn.setAttribute("aria-label", "Increase quantity");
|
|
244
|
-
qtyControl.append(minusBtn, qtyInput, plusBtn);
|
|
245
|
-
qtyWrapper.appendChild(qtyControl);
|
|
246
|
-
container.appendChild(qtyWrapper);
|
|
247
|
-
const button = document.createElement("button");
|
|
248
|
-
button.className = "lb-bundle__cta";
|
|
249
|
-
button.setAttribute("part", "button");
|
|
250
|
-
container.appendChild(button);
|
|
251
|
-
function updateTiers() {
|
|
252
|
-
const savings = calculateTierSavings(
|
|
253
|
-
bundle.volumeTiers,
|
|
254
|
-
basePrice,
|
|
255
|
-
quantity
|
|
256
|
-
);
|
|
257
|
-
tiersDiv.innerHTML = "";
|
|
258
|
-
for (const ts of savings) {
|
|
259
|
-
const row = document.createElement("div");
|
|
260
|
-
row.className = `lb-bundle__tier${ts.isActive ? " lb-bundle__tier--active" : ""}`;
|
|
261
|
-
row.setAttribute("role", "row");
|
|
262
|
-
row.innerHTML = `
|
|
263
|
-
<span class="lb-bundle__tier-quantity" role="cell">${ts.tier.minQuantity}+ items</span>
|
|
264
|
-
<span class="lb-bundle__tier-price" role="cell">${escapeHtml3(formatMoney3(ts.unitPrice, currency))} each</span>
|
|
265
|
-
<span class="lb-bundle__tier-savings" role="cell">Save ${ts.savingsPercent.toFixed(0)}%</span>
|
|
266
|
-
${ts.tier.label ? `<span class="lb-bundle__tier-label" role="cell">${escapeHtml3(ts.tier.label)}</span>` : ""}
|
|
267
|
-
`;
|
|
268
|
-
tiersDiv.appendChild(row);
|
|
269
|
-
}
|
|
270
|
-
button.textContent = bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`;
|
|
271
|
-
}
|
|
272
|
-
updateTiers();
|
|
273
|
-
minusBtn.addEventListener("click", () => {
|
|
274
|
-
if (quantity > 1) {
|
|
275
|
-
quantity--;
|
|
276
|
-
qtyInput.value = String(quantity);
|
|
277
|
-
updateTiers();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
plusBtn.addEventListener("click", () => {
|
|
281
|
-
quantity++;
|
|
282
|
-
qtyInput.value = String(quantity);
|
|
283
|
-
updateTiers();
|
|
284
|
-
});
|
|
285
|
-
qtyInput.addEventListener("change", () => {
|
|
286
|
-
const val = parseInt(qtyInput.value, 10);
|
|
287
|
-
if (!isNaN(val) && val > 0) {
|
|
288
|
-
quantity = val;
|
|
289
|
-
updateTiers();
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
button.addEventListener("click", () => {
|
|
293
|
-
const variant = product.variants.nodes.find((v) => v.availableForSale);
|
|
294
|
-
if (!variant) return;
|
|
295
|
-
onAddToCart([
|
|
296
|
-
{
|
|
297
|
-
merchandiseId: variant.id,
|
|
298
|
-
quantity,
|
|
299
|
-
attributes: [
|
|
300
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
301
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
302
|
-
]
|
|
303
|
-
}
|
|
304
|
-
]);
|
|
305
|
-
});
|
|
2382
|
+
.lb-volume__tiers {
|
|
2383
|
+
display: flex;
|
|
2384
|
+
flex-direction: column;
|
|
2385
|
+
gap: 12px;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
.lb-volume__tier {
|
|
2389
|
+
display: flex;
|
|
2390
|
+
align-items: center;
|
|
2391
|
+
gap: 12px;
|
|
2392
|
+
border: var(--lb-tier-border-width) solid var(--lb-tier-border-color);
|
|
2393
|
+
border-radius: var(--lb-tier-radius);
|
|
2394
|
+
padding: 12px 16px;
|
|
2395
|
+
cursor: pointer;
|
|
2396
|
+
position: relative;
|
|
2397
|
+
transition: border-color 0.15s ease;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
.lb-volume__tier:hover {
|
|
2401
|
+
border-color: color-mix(in srgb, var(--lb-tier-border-color) 50%, black);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
.lb-volume__tier:focus-visible {
|
|
2405
|
+
outline: 2px solid var(--lb-primary-color);
|
|
2406
|
+
outline-offset: 2px;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
.lb-volume__tier[aria-checked="true"] {
|
|
2410
|
+
border-color: var(--lb-tier-selected-border-color);
|
|
2411
|
+
outline: var(--lb-tier-selected-border-width) solid var(--lb-tier-selected-border-color);
|
|
2412
|
+
outline-offset: calc(-1 * var(--lb-tier-selected-border-width));
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.lb-volume__radio {
|
|
2416
|
+
width: 20px;
|
|
2417
|
+
height: 20px;
|
|
2418
|
+
min-width: 20px;
|
|
2419
|
+
border: 2px solid var(--lb-text);
|
|
2420
|
+
border-radius: 50%;
|
|
2421
|
+
display: flex;
|
|
2422
|
+
align-items: center;
|
|
2423
|
+
justify-content: center;
|
|
2424
|
+
transition: border-color 0.15s ease;
|
|
306
2425
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
2426
|
+
|
|
2427
|
+
.lb-volume__tier[aria-checked="true"] .lb-volume__radio {
|
|
2428
|
+
border-color: var(--lb-text);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
.lb-volume__radio-dot {
|
|
2432
|
+
width: 12px;
|
|
2433
|
+
height: 12px;
|
|
2434
|
+
border-radius: 50%;
|
|
2435
|
+
background: transparent;
|
|
2436
|
+
transition: background 0.15s ease;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
.lb-volume__tier[aria-checked="true"] .lb-volume__radio-dot {
|
|
2440
|
+
background: var(--lb-text);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/* Tier content: 2x2 grid layout */
|
|
2444
|
+
.lb-volume__tier-grid {
|
|
2445
|
+
flex: 1;
|
|
2446
|
+
display: grid;
|
|
2447
|
+
row-gap: 4px;
|
|
2448
|
+
align-items: center;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
.lb-volume__tier-label {
|
|
2452
|
+
font-size: 16px;
|
|
2453
|
+
font-weight: 700;
|
|
2454
|
+
color: var(--lb-text);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
.lb-volume__tier-badge {
|
|
2458
|
+
flex-shrink: 0;
|
|
2459
|
+
font-size: 12px;
|
|
2460
|
+
line-height: 16px;
|
|
2461
|
+
color: var(--lb-popular-badge-text);
|
|
2462
|
+
background: var(--lb-popular-badge-bg);
|
|
2463
|
+
border: var(--lb-popular-badge-border-width) solid var(--lb-popular-badge-border-color);
|
|
2464
|
+
border-radius: var(--lb-popular-badge-radius);
|
|
2465
|
+
padding: 4px 8px;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
.lb-volume__tier-price {
|
|
2469
|
+
grid-column: 1 / -1;
|
|
2470
|
+
font-size: 12px;
|
|
2471
|
+
font-weight: 500;
|
|
2472
|
+
color: var(--lb-text);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
.lb-volume__tier-unit {
|
|
2476
|
+
font-size: 12px;
|
|
2477
|
+
color: var(--lb-text);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/* Compare-at (strikethrough) price */
|
|
2481
|
+
.lb-volume__tier-compare {
|
|
2482
|
+
font-size: 12px;
|
|
2483
|
+
line-height: 16px;
|
|
2484
|
+
color: color-mix(in srgb, var(--lb-text) 60%, transparent);
|
|
2485
|
+
text-decoration: line-through;
|
|
2486
|
+
margin-right: 4px;
|
|
311
2487
|
}
|
|
312
2488
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
2489
|
+
|
|
2490
|
+
`;
|
|
2491
|
+
var BUNDLE_SKELETON_CSS = `/* Lime Bundles \u2014 web-component loading skeleton (not mirrored to theme assets) */
|
|
2492
|
+
|
|
2493
|
+
.lb-bundle-widget--loading {
|
|
316
2494
|
display: block;
|
|
317
|
-
--lb-
|
|
318
|
-
--lb-
|
|
319
|
-
--lb-
|
|
320
|
-
--lb-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.lb-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.lb-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.lb-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.lb-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.lb-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
2495
|
+
padding: var(--lb-widget-pad, 20px);
|
|
2496
|
+
border: 1px solid var(--lb-border, #E5E5E5);
|
|
2497
|
+
border-radius: var(--lb-radius, 12px);
|
|
2498
|
+
background: var(--lb-bg, #FFFFFF);
|
|
2499
|
+
/* Contain layout/paint so the skeleton doesn't influence ancestor
|
|
2500
|
+
layout once the real content swaps in. */
|
|
2501
|
+
contain: layout paint;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
.lb-bundle-widget--loading .lb-skeleton {
|
|
2505
|
+
background: linear-gradient(
|
|
2506
|
+
90deg,
|
|
2507
|
+
rgba(0, 0, 0, 0.06) 0%,
|
|
2508
|
+
rgba(0, 0, 0, 0.10) 50%,
|
|
2509
|
+
rgba(0, 0, 0, 0.06) 100%
|
|
2510
|
+
);
|
|
2511
|
+
background-size: 200% 100%;
|
|
2512
|
+
border-radius: 6px;
|
|
2513
|
+
animation: lb-skeleton-pulse 1.4s ease-in-out infinite;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.lb-bundle-widget--loading .lb-skeleton--title {
|
|
2517
|
+
height: 28px;
|
|
2518
|
+
width: 60%;
|
|
2519
|
+
margin-bottom: 16px;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
.lb-bundle-widget--loading .lb-skeleton--products {
|
|
2523
|
+
display: grid;
|
|
2524
|
+
gap: 12px;
|
|
2525
|
+
margin-bottom: 16px;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
.lb-bundle-widget--loading .lb-skeleton--row {
|
|
2529
|
+
display: grid;
|
|
2530
|
+
grid-template-columns: 56px 1fr 60px;
|
|
2531
|
+
gap: 12px;
|
|
2532
|
+
align-items: center;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
.lb-bundle-widget--loading .lb-skeleton--thumb {
|
|
2536
|
+
height: 56px;
|
|
2537
|
+
width: 56px;
|
|
2538
|
+
border-radius: 8px;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
.lb-bundle-widget--loading .lb-skeleton--line {
|
|
2542
|
+
height: 14px;
|
|
2543
|
+
border-radius: 4px;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
.lb-bundle-widget--loading .lb-skeleton--line + .lb-skeleton--line {
|
|
2547
|
+
margin-top: 8px;
|
|
2548
|
+
width: 70%;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
.lb-bundle-widget--loading .lb-skeleton--price {
|
|
2552
|
+
height: 20px;
|
|
2553
|
+
width: 60px;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
.lb-bundle-widget--loading .lb-skeleton--footer {
|
|
2557
|
+
margin-top: 16px;
|
|
2558
|
+
padding-top: 16px;
|
|
2559
|
+
border-top: 1px solid var(--lb-border, #E5E5E5);
|
|
2560
|
+
display: grid;
|
|
2561
|
+
grid-template-columns: 1fr auto;
|
|
2562
|
+
gap: 12px;
|
|
2563
|
+
align-items: center;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
.lb-bundle-widget--loading .lb-skeleton--total {
|
|
2567
|
+
height: 24px;
|
|
2568
|
+
width: 40%;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
.lb-bundle-widget--loading .lb-skeleton--cta {
|
|
2572
|
+
height: 44px;
|
|
2573
|
+
width: 140px;
|
|
2574
|
+
border-radius: var(--lb-cta-radius, 8px);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
@keyframes lb-skeleton-pulse {
|
|
2578
|
+
0% { background-position: 0% 50%; }
|
|
2579
|
+
100% { background-position: -200% 50%; }
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2583
|
+
.lb-bundle-widget--loading .lb-skeleton {
|
|
2584
|
+
animation: none;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
377
2587
|
`;
|
|
378
2588
|
|
|
379
2589
|
// src/lime-bundle.ts
|
|
@@ -406,22 +2616,38 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
406
2616
|
bundles = [];
|
|
407
2617
|
abortController = null;
|
|
408
2618
|
impressionCleanups = [];
|
|
2619
|
+
/**
|
|
2620
|
+
* Tick / observer / listener cleanups registered by individual renderers
|
|
2621
|
+
* (e.g. countdown setInterval handles). Flushed in disconnectedCallback
|
|
2622
|
+
* so we never leak timers or event listeners when the widget is removed.
|
|
2623
|
+
*/
|
|
2624
|
+
renderCleanups = [];
|
|
2625
|
+
/**
|
|
2626
|
+
* Merchant custom CSS fetched from the shop metafield. Injected inside
|
|
2627
|
+
* the shadow root alongside the bundle stylesheets so selectors like
|
|
2628
|
+
* `.lb-bundle-widget { ... }` reach the widget's DOM.
|
|
2629
|
+
*/
|
|
2630
|
+
shopCustomCss = null;
|
|
409
2631
|
constructor() {
|
|
410
2632
|
super();
|
|
411
2633
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
412
2634
|
}
|
|
413
2635
|
connectedCallback() {
|
|
414
|
-
this.render();
|
|
415
2636
|
this.fetchBundle();
|
|
416
2637
|
}
|
|
417
2638
|
disconnectedCallback() {
|
|
418
2639
|
this.abortController?.abort();
|
|
419
2640
|
this.teardownImpressions();
|
|
2641
|
+
this.teardownRenderers();
|
|
420
2642
|
}
|
|
421
2643
|
teardownImpressions() {
|
|
422
2644
|
for (const cleanup of this.impressionCleanups) cleanup();
|
|
423
2645
|
this.impressionCleanups = [];
|
|
424
2646
|
}
|
|
2647
|
+
teardownRenderers() {
|
|
2648
|
+
for (const cleanup of this.renderCleanups) cleanup();
|
|
2649
|
+
this.renderCleanups = [];
|
|
2650
|
+
}
|
|
425
2651
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
426
2652
|
if (oldValue === newValue || !this.isConnected) return;
|
|
427
2653
|
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
@@ -497,7 +2723,10 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
497
2723
|
const css = await cssPromise;
|
|
498
2724
|
if (css?.shop?.metafield?.value) {
|
|
499
2725
|
injectCustomCss(this.shopDomain, css.shop.metafield.value);
|
|
2726
|
+
const sanitized = sanitizeCustomCss(css.shop.metafield.value);
|
|
2727
|
+
if (sanitized.ok) this.shopCustomCss = sanitized.css;
|
|
500
2728
|
}
|
|
2729
|
+
await this.applyABVariants();
|
|
501
2730
|
this.renderBundles();
|
|
502
2731
|
} catch (err) {
|
|
503
2732
|
if (controller.signal.aborted) return;
|
|
@@ -508,6 +2737,40 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
508
2737
|
);
|
|
509
2738
|
}
|
|
510
2739
|
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Resolve the visitor's A/B bucket for every bundle with an active test
|
|
2742
|
+
* and merge Variant B overrides where applicable. Runs in parallel; any
|
|
2743
|
+
* assignment failure logs internally but still renders Variant A (safe
|
|
2744
|
+
* default). The `getABTestAssignment` helper also persists the bucket
|
|
2745
|
+
* via a first-party cookie + POSTs to /api/ab-assign for server-side
|
|
2746
|
+
* analytics.
|
|
2747
|
+
*/
|
|
2748
|
+
async applyABVariants() {
|
|
2749
|
+
if (!this.appUrl || this.bundles.length === 0) return;
|
|
2750
|
+
const results = await Promise.all(
|
|
2751
|
+
this.bundles.map(async (bundle) => {
|
|
2752
|
+
if (!bundle.abTestId || !bundle.abVariantB) return bundle;
|
|
2753
|
+
try {
|
|
2754
|
+
const assignment = await getABTestAssignment(
|
|
2755
|
+
this.appUrl,
|
|
2756
|
+
this.shopDomain,
|
|
2757
|
+
bundle.abTestId,
|
|
2758
|
+
bundle.id
|
|
2759
|
+
);
|
|
2760
|
+
if (assignment?.variant === "B") {
|
|
2761
|
+
return applyABVariantB(bundle);
|
|
2762
|
+
}
|
|
2763
|
+
} catch (err) {
|
|
2764
|
+
console.warn(
|
|
2765
|
+
`[lime-bundle] A/B assignment failed for bundle ${bundle.id}; falling back to Variant A.`,
|
|
2766
|
+
err
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
return bundle;
|
|
2770
|
+
})
|
|
2771
|
+
);
|
|
2772
|
+
this.bundles = results;
|
|
2773
|
+
}
|
|
511
2774
|
async fetchSingleBundle(client, signal) {
|
|
512
2775
|
const data = await client.query(
|
|
513
2776
|
BUNDLE_METAOBJECT_QUERY,
|
|
@@ -652,29 +2915,41 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
652
2915
|
}
|
|
653
2916
|
renderBundles() {
|
|
654
2917
|
this.teardownImpressions();
|
|
2918
|
+
this.teardownRenderers();
|
|
655
2919
|
this.shadow.innerHTML = "";
|
|
656
2920
|
const style = document.createElement("style");
|
|
657
|
-
style.textContent =
|
|
2921
|
+
style.textContent = [
|
|
2922
|
+
BUNDLE_BASE_CSS,
|
|
2923
|
+
BUNDLE_FIXED_CSS,
|
|
2924
|
+
BUNDLE_MIX_MATCH_CSS,
|
|
2925
|
+
BUNDLE_VOLUME_CSS
|
|
2926
|
+
].join("\n");
|
|
658
2927
|
this.shadow.appendChild(style);
|
|
2928
|
+
if (this.shopCustomCss) {
|
|
2929
|
+
const customStyle = document.createElement("style");
|
|
2930
|
+
customStyle.setAttribute("data-lime-bundles", "shop-custom-css");
|
|
2931
|
+
customStyle.textContent = this.shopCustomCss;
|
|
2932
|
+
this.shadow.appendChild(customStyle);
|
|
2933
|
+
}
|
|
659
2934
|
for (const bundle of this.bundles) {
|
|
660
2935
|
const container = document.createElement("div");
|
|
661
|
-
container.className = "lb-bundle";
|
|
2936
|
+
container.className = "lb-bundle-widget";
|
|
662
2937
|
container.setAttribute("role", "region");
|
|
663
2938
|
container.setAttribute("aria-label", bundle.title);
|
|
2939
|
+
container.setAttribute("data-bundle-type", bundle.bundleType);
|
|
2940
|
+
container.setAttribute("data-bundle-gid", bundle.id);
|
|
2941
|
+
applyWidgetConfigVars(container, bundle.widgetConfig);
|
|
664
2942
|
const dispatch = (lines) => this.handleAddToCart(bundle, lines);
|
|
2943
|
+
const registerCleanup = (fn) => this.renderCleanups.push(fn);
|
|
665
2944
|
switch (bundle.bundleType) {
|
|
666
2945
|
case "fixed":
|
|
667
|
-
renderFixedBundle(container, bundle, dispatch);
|
|
2946
|
+
renderFixedBundle(container, bundle, dispatch, registerCleanup);
|
|
668
2947
|
break;
|
|
669
2948
|
case "mix_match":
|
|
670
|
-
renderMixMatchBundle(
|
|
671
|
-
container,
|
|
672
|
-
bundle,
|
|
673
|
-
dispatch
|
|
674
|
-
);
|
|
2949
|
+
renderMixMatchBundle(container, bundle, dispatch, registerCleanup);
|
|
675
2950
|
break;
|
|
676
2951
|
case "volume":
|
|
677
|
-
renderVolumeBundle(container, bundle, dispatch);
|
|
2952
|
+
renderVolumeBundle(container, bundle, dispatch, registerCleanup);
|
|
678
2953
|
break;
|
|
679
2954
|
}
|
|
680
2955
|
this.shadow.appendChild(container);
|
|
@@ -711,10 +2986,40 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
711
2986
|
}
|
|
712
2987
|
renderLoading() {
|
|
713
2988
|
this.shadow.innerHTML = `
|
|
714
|
-
<style>${
|
|
715
|
-
<
|
|
2989
|
+
<style>${BUNDLE_BASE_CSS}</style>
|
|
2990
|
+
<style>${BUNDLE_SKELETON_CSS}</style>
|
|
2991
|
+
<div class="lb-bundle-widget lb-bundle-widget--loading" aria-busy="true" aria-label="Loading bundle">
|
|
716
2992
|
<div class="lb-skeleton lb-skeleton--title"></div>
|
|
717
|
-
<div class="lb-skeleton
|
|
2993
|
+
<div class="lb-skeleton--products">
|
|
2994
|
+
<div class="lb-skeleton--row">
|
|
2995
|
+
<div class="lb-skeleton lb-skeleton--thumb"></div>
|
|
2996
|
+
<div>
|
|
2997
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
2998
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
2999
|
+
</div>
|
|
3000
|
+
<div class="lb-skeleton lb-skeleton--price"></div>
|
|
3001
|
+
</div>
|
|
3002
|
+
<div class="lb-skeleton--row">
|
|
3003
|
+
<div class="lb-skeleton lb-skeleton--thumb"></div>
|
|
3004
|
+
<div>
|
|
3005
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
3006
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
3007
|
+
</div>
|
|
3008
|
+
<div class="lb-skeleton lb-skeleton--price"></div>
|
|
3009
|
+
</div>
|
|
3010
|
+
<div class="lb-skeleton--row">
|
|
3011
|
+
<div class="lb-skeleton lb-skeleton--thumb"></div>
|
|
3012
|
+
<div>
|
|
3013
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
3014
|
+
<div class="lb-skeleton lb-skeleton--line"></div>
|
|
3015
|
+
</div>
|
|
3016
|
+
<div class="lb-skeleton lb-skeleton--price"></div>
|
|
3017
|
+
</div>
|
|
3018
|
+
</div>
|
|
3019
|
+
<div class="lb-skeleton--footer">
|
|
3020
|
+
<div class="lb-skeleton lb-skeleton--total"></div>
|
|
3021
|
+
<div class="lb-skeleton lb-skeleton--cta"></div>
|
|
3022
|
+
</div>
|
|
718
3023
|
</div>
|
|
719
3024
|
`;
|
|
720
3025
|
}
|
|
@@ -728,9 +3033,6 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
728
3033
|
})
|
|
729
3034
|
);
|
|
730
3035
|
}
|
|
731
|
-
render() {
|
|
732
|
-
this.shadow.innerHTML = `<style>${WIDGET_STYLES}</style>`;
|
|
733
|
-
}
|
|
734
3036
|
};
|
|
735
3037
|
|
|
736
3038
|
// src/index.ts
|