@lime-bundles/widget 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,16 +26,393 @@ __export(index_exports, {
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
28
  // src/lime-bundle.ts
29
- var import_core6 = require("@lime-bundles/core");
29
+ var import_core7 = require("@lime-bundles/core");
30
30
 
31
31
  // src/renderers/fixed.ts
32
- var import_core4 = require("@lime-bundles/core");
32
+ var import_core5 = require("@lime-bundles/core");
33
33
 
34
- // src/renderers/pricing.ts
34
+ // src/dropdown/bind-dropdown.ts
35
35
  var import_core = require("@lime-bundles/core");
36
+ var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = import_core.dropdown;
37
+ var ITEM_HEIGHT_PX = 32;
38
+ var LIST_PAD_Y = 8;
39
+ var MAX_VISIBLE_ITEMS = 8;
40
+ var openInstances = [];
41
+ function closeOutsideEvent(event) {
42
+ const path = event.composedPath();
43
+ for (let i = openInstances.length - 1; i >= 0; i--) {
44
+ const inst = openInstances[i];
45
+ if (!path.includes(inst.shell) && !path.includes(inst.listbox)) {
46
+ inst.close();
47
+ }
48
+ }
49
+ }
50
+ function onDocResize() {
51
+ for (let i = openInstances.length - 1; i >= 0; i--) openInstances[i].close();
52
+ }
53
+ var docListenersAttached = false;
54
+ function attachDocumentListeners() {
55
+ if (docListenersAttached) return;
56
+ document.addEventListener("pointerdown", closeOutsideEvent, true);
57
+ window.addEventListener("scroll", closeOutsideEvent, true);
58
+ window.addEventListener("resize", onDocResize);
59
+ docListenersAttached = true;
60
+ }
61
+ function detachDocumentListeners() {
62
+ if (!docListenersAttached || openInstances.length > 0) return;
63
+ document.removeEventListener("pointerdown", closeOutsideEvent, true);
64
+ window.removeEventListener("scroll", closeOutsideEvent, true);
65
+ window.removeEventListener("resize", onDocResize);
66
+ docListenersAttached = false;
67
+ }
68
+ function readOptions(select) {
69
+ const out = [];
70
+ for (let i = 0; i < select.options.length; i++) {
71
+ const o = select.options[i];
72
+ out.push({ disabled: o.disabled, label: o.textContent || o.value });
73
+ }
74
+ return out;
75
+ }
76
+ function firstEnabled(opts) {
77
+ for (let i = 0; i < opts.length; i++) if (!opts[i].disabled) return i;
78
+ return -1;
79
+ }
80
+ var VARIANT_SELECT_CLASSES = [
81
+ "lb-bundle-variant-select",
82
+ "lb-mix-match__variant-select"
83
+ ];
84
+ var BIND_SELECTOR = VARIANT_SELECT_CLASSES.map(
85
+ (c) => `select.${c}:not(.lb-dropdown-state)`
86
+ ).join(", ");
87
+ function bindDropdown(select) {
88
+ const slot = select;
89
+ if (select.classList.contains("lb-dropdown-state")) {
90
+ return slot.__lbDropdownInstance ?? null;
91
+ }
92
+ const doc = select.ownerDocument;
93
+ const rootNode = select.getRootNode();
94
+ const labelText = select.getAttribute("aria-label") ?? "";
95
+ const idBase = `lb-dd-${Math.random().toString(36).slice(2, 9)}`;
96
+ select.classList.add("lb-dropdown-state");
97
+ select.setAttribute("aria-hidden", "true");
98
+ select.setAttribute("tabindex", "-1");
99
+ const shell = doc.createElement("div");
100
+ shell.className = "lb-dropdown";
101
+ shell.setAttribute("data-lb-dropdown", "");
102
+ const trigger = doc.createElement("button");
103
+ trigger.type = "button";
104
+ trigger.className = "lb-dropdown-trigger";
105
+ trigger.setAttribute("role", "combobox");
106
+ trigger.setAttribute("aria-haspopup", "listbox");
107
+ trigger.setAttribute("aria-expanded", "false");
108
+ const listboxId = `${idBase}-listbox`;
109
+ trigger.setAttribute("aria-controls", listboxId);
110
+ if (labelText) trigger.setAttribute("aria-label", labelText);
111
+ const triggerLabel = doc.createElement("span");
112
+ triggerLabel.className = "lb-dropdown-trigger-value";
113
+ const chevron = doc.createElement("span");
114
+ chevron.className = "lb-dropdown-chevron";
115
+ chevron.setAttribute("aria-hidden", "true");
116
+ trigger.appendChild(triggerLabel);
117
+ trigger.appendChild(chevron);
118
+ const listbox = doc.createElement("ul");
119
+ listbox.id = listboxId;
120
+ listbox.className = "lb-dropdown-listbox";
121
+ listbox.setAttribute("role", "listbox");
122
+ if (labelText) listbox.setAttribute("aria-label", labelText);
123
+ listbox.hidden = true;
124
+ shell.appendChild(trigger);
125
+ select.parentNode?.insertBefore(shell, select.nextSibling);
126
+ const modalOverlay = select.closest("[data-modal-overlay]");
127
+ if (modalOverlay) {
128
+ modalOverlay.appendChild(listbox);
129
+ listbox.setAttribute("data-lb-dropdown-portal", "");
130
+ } else {
131
+ shell.appendChild(listbox);
132
+ }
133
+ let isOpen = false;
134
+ let activeIndex = -1;
135
+ let typeAhead = emptyTypeAheadState();
136
+ let optionEls = [];
137
+ let instance;
138
+ function syncFromSelect() {
139
+ const opts = readOptions(select);
140
+ const idx = select.selectedIndex;
141
+ triggerLabel.textContent = idx >= 0 && opts[idx] ? opts[idx].label : "";
142
+ while (listbox.firstChild) listbox.removeChild(listbox.firstChild);
143
+ optionEls = [];
144
+ for (let i = 0; i < opts.length; i++) {
145
+ const li = doc.createElement("li");
146
+ li.id = `${idBase}-opt-${i}`;
147
+ li.className = "lb-dropdown-option";
148
+ li.setAttribute("role", "option");
149
+ li.setAttribute("aria-selected", i === idx ? "true" : "false");
150
+ if (opts[i].disabled) li.setAttribute("aria-disabled", "true");
151
+ li.setAttribute("data-value", select.options[i].value);
152
+ li.setAttribute("data-index", String(i));
153
+ li.textContent = opts[i].label;
154
+ listbox.appendChild(li);
155
+ optionEls.push(li);
156
+ }
157
+ }
158
+ function setActive(newIndex) {
159
+ if (activeIndex >= 0 && optionEls[activeIndex]) {
160
+ optionEls[activeIndex].classList.remove("is-active");
161
+ }
162
+ activeIndex = newIndex;
163
+ if (newIndex >= 0 && optionEls[newIndex]) {
164
+ const li = optionEls[newIndex];
165
+ li.classList.add("is-active");
166
+ trigger.setAttribute("aria-activedescendant", li.id);
167
+ const liTop = li.offsetTop;
168
+ const liBottom = liTop + li.offsetHeight;
169
+ const visTop = listbox.scrollTop;
170
+ const visBottom = visTop + listbox.clientHeight;
171
+ if (liTop < visTop) {
172
+ listbox.scrollTop = liTop;
173
+ } else if (liBottom > visBottom) {
174
+ listbox.scrollTop = liBottom - listbox.clientHeight;
175
+ }
176
+ } else {
177
+ trigger.setAttribute("aria-activedescendant", "");
178
+ }
179
+ }
180
+ function position() {
181
+ const rect = trigger.getBoundingClientRect();
182
+ if (rect.width === 0) return false;
183
+ const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);
184
+ const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
185
+ const result = computePosition({
186
+ trigger: {
187
+ top: rect.top,
188
+ bottom: rect.bottom,
189
+ left: rect.left,
190
+ width: rect.width
191
+ },
192
+ viewportHeight: window.innerHeight,
193
+ desiredHeight
194
+ });
195
+ listbox.setAttribute("data-placement", result.placement);
196
+ listbox.style.maxHeight = `${result.maxHeight}px`;
197
+ if (listbox.hasAttribute("data-lb-dropdown-portal")) {
198
+ listbox.style.top = `${result.offsetTop}px`;
199
+ listbox.style.left = `${result.offsetLeft}px`;
200
+ listbox.style.width = `${result.width}px`;
201
+ }
202
+ return true;
203
+ }
204
+ function open() {
205
+ if (isOpen) return;
206
+ for (let i = openInstances.length - 1; i >= 0; i--) {
207
+ if (openInstances[i] !== instance) openInstances[i].close();
208
+ }
209
+ isOpen = true;
210
+ listbox.hidden = false;
211
+ trigger.setAttribute("aria-expanded", "true");
212
+ if (!position()) {
213
+ requestAnimationFrame(() => position());
214
+ }
215
+ const opts = readOptions(select);
216
+ const selIdx = select.selectedIndex;
217
+ if (selIdx >= 0 && opts[selIdx] && !opts[selIdx].disabled) {
218
+ setActive(selIdx);
219
+ } else {
220
+ setActive(firstEnabled(opts));
221
+ }
222
+ openInstances.push(instance);
223
+ if (openInstances.length === 1) attachDocumentListeners();
224
+ }
225
+ function close(restoreFocus) {
226
+ if (!isOpen) return;
227
+ isOpen = false;
228
+ listbox.hidden = true;
229
+ trigger.setAttribute("aria-expanded", "false");
230
+ trigger.setAttribute("aria-activedescendant", "");
231
+ if (activeIndex >= 0 && optionEls[activeIndex]) {
232
+ optionEls[activeIndex].classList.remove("is-active");
233
+ }
234
+ activeIndex = -1;
235
+ const idx = openInstances.indexOf(instance);
236
+ if (idx >= 0) openInstances.splice(idx, 1);
237
+ if (openInstances.length === 0) detachDocumentListeners();
238
+ if (restoreFocus) trigger.focus();
239
+ }
240
+ function commit(index) {
241
+ const opt = select.options[index];
242
+ if (!opt || opt.disabled) return;
243
+ if (select.value !== opt.value) {
244
+ select.value = opt.value;
245
+ const event = new Event("change", { bubbles: true });
246
+ select.dispatchEvent(event);
247
+ }
248
+ syncFromSelect();
249
+ close(true);
250
+ }
251
+ function applyAction(action) {
252
+ switch (action.type) {
253
+ case "open":
254
+ open();
255
+ if (action.activeIndex >= 0) setActive(action.activeIndex);
256
+ return;
257
+ case "close":
258
+ close(action.restoreFocus);
259
+ return;
260
+ case "move-active":
261
+ setActive(action.activeIndex);
262
+ return;
263
+ case "commit":
264
+ commit(action.index);
265
+ return;
266
+ case "type-ahead": {
267
+ const opts = readOptions(select);
268
+ const result = pushTypeAheadChar(
269
+ typeAhead,
270
+ action.char,
271
+ Date.now(),
272
+ opts
273
+ );
274
+ typeAhead = result.newState;
275
+ if (result.matchedIndex !== null) {
276
+ if (!isOpen) open();
277
+ setActive(result.matchedIndex);
278
+ }
279
+ return;
280
+ }
281
+ case "passthrough":
282
+ return;
283
+ default: {
284
+ const _exhaustive = action;
285
+ void _exhaustive;
286
+ }
287
+ }
288
+ }
289
+ function onKeydown(event) {
290
+ const opts = readOptions(select);
291
+ const action = handleKey(
292
+ {
293
+ key: event.key,
294
+ ctrlKey: event.ctrlKey,
295
+ metaKey: event.metaKey,
296
+ altKey: event.altKey,
297
+ shiftKey: event.shiftKey
298
+ },
299
+ {
300
+ isOpen,
301
+ activeIndex,
302
+ selectedIndex: select.selectedIndex,
303
+ options: opts
304
+ }
305
+ );
306
+ if (action.preventDefault) event.preventDefault();
307
+ applyAction(action);
308
+ }
309
+ function onTriggerClick(event) {
310
+ event.preventDefault();
311
+ if (isOpen) close(false);
312
+ else open();
313
+ }
314
+ function onListboxClick(event) {
315
+ let target = event.target;
316
+ while (target && target !== listbox) {
317
+ if (target.classList?.contains("lb-dropdown-option")) {
318
+ const idx = parseInt(target.getAttribute("data-index") ?? "", 10);
319
+ if (!Number.isNaN(idx)) {
320
+ commit(idx);
321
+ return;
322
+ }
323
+ }
324
+ target = target.parentElement;
325
+ }
326
+ }
327
+ function onListboxMousemove(event) {
328
+ let target = event.target;
329
+ while (target && target !== listbox) {
330
+ if (target.classList?.contains("lb-dropdown-option")) {
331
+ if (target.getAttribute("aria-disabled") === "true") return;
332
+ const idx = parseInt(target.getAttribute("data-index") ?? "", 10);
333
+ if (!Number.isNaN(idx) && idx !== activeIndex) setActive(idx);
334
+ return;
335
+ }
336
+ target = target.parentElement;
337
+ }
338
+ }
339
+ function onShellFocusout() {
340
+ setTimeout(() => {
341
+ if (!isOpen) return;
342
+ const active = rootNode.activeElement ?? doc.activeElement;
343
+ if (!shell.contains(active)) close(false);
344
+ }, 0);
345
+ }
346
+ function onSelectChange() {
347
+ syncFromSelect();
348
+ }
349
+ const observer = new MutationObserver(() => {
350
+ syncFromSelect();
351
+ });
352
+ observer.observe(select, {
353
+ childList: true,
354
+ subtree: true,
355
+ attributes: true,
356
+ attributeFilter: ["disabled", "value", "selected"]
357
+ });
358
+ const onListboxMousedown = (event) => event.preventDefault();
359
+ trigger.addEventListener("click", onTriggerClick);
360
+ trigger.addEventListener("keydown", onKeydown);
361
+ shell.addEventListener("focusout", onShellFocusout);
362
+ listbox.addEventListener("mousedown", onListboxMousedown);
363
+ listbox.addEventListener("click", onListboxClick);
364
+ listbox.addEventListener("mousemove", onListboxMousemove);
365
+ select.addEventListener("change", onSelectChange);
366
+ function destroy() {
367
+ if (isOpen) close(false);
368
+ observer.disconnect();
369
+ trigger.removeEventListener("click", onTriggerClick);
370
+ trigger.removeEventListener("keydown", onKeydown);
371
+ shell.removeEventListener("focusout", onShellFocusout);
372
+ listbox.removeEventListener("mousedown", onListboxMousedown);
373
+ listbox.removeEventListener("click", onListboxClick);
374
+ listbox.removeEventListener("mousemove", onListboxMousemove);
375
+ select.removeEventListener("change", onSelectChange);
376
+ if (shell.parentNode) shell.parentNode.removeChild(shell);
377
+ if (listbox.parentNode) listbox.parentNode.removeChild(listbox);
378
+ select.classList.remove("lb-dropdown-state");
379
+ select.removeAttribute("aria-hidden");
380
+ select.removeAttribute("tabindex");
381
+ delete slot.__lbDropdownInstance;
382
+ }
383
+ instance = {
384
+ shell,
385
+ listbox,
386
+ select,
387
+ close: () => close(false),
388
+ destroy
389
+ };
390
+ slot.__lbDropdownInstance = instance;
391
+ syncFromSelect();
392
+ return instance;
393
+ }
394
+ function bindAllDropdowns(root) {
395
+ const selects = root.querySelectorAll(BIND_SELECTOR);
396
+ const instances = [];
397
+ selects.forEach((sel) => {
398
+ const inst = bindDropdown(sel);
399
+ if (inst) instances.push(inst);
400
+ });
401
+ return instances;
402
+ }
403
+ function unbindAllDropdowns(root) {
404
+ const bound = root.querySelectorAll("select.lb-dropdown-state");
405
+ bound.forEach((sel) => {
406
+ const inst = sel.__lbDropdownInstance;
407
+ if (inst) inst.destroy();
408
+ });
409
+ }
36
410
 
37
- // src/renderers/countdown.ts
411
+ // src/renderers/pricing.ts
38
412
  var import_core2 = require("@lime-bundles/core");
413
+
414
+ // src/renderers/countdown.ts
415
+ var import_core3 = require("@lime-bundles/core");
39
416
  function renderCountdown(endsAtIso) {
40
417
  const parsed = parseIso(endsAtIso);
41
418
  if (parsed === null) return null;
@@ -62,7 +439,7 @@ function renderCountdown(endsAtIso) {
62
439
  stop();
63
440
  return;
64
441
  }
65
- timer.textContent = (0, import_core2.formatCountdown)(msLeft);
442
+ timer.textContent = (0, import_core3.formatCountdown)(msLeft);
66
443
  }
67
444
  function stop() {
68
445
  if (intervalId !== null) {
@@ -116,7 +493,7 @@ function el(tag, className, attrs = {}) {
116
493
  }
117
494
 
118
495
  // src/renderers/image.ts
119
- var import_core3 = require("@lime-bundles/core");
496
+ var import_core4 = require("@lime-bundles/core");
120
497
 
121
498
  // src/renderers/fixed.ts
122
499
  var PLACEHOLDER_THUMB_SVG = `
@@ -127,7 +504,7 @@ var PLACEHOLDER_THUMB_SVG = `
127
504
  </svg>`;
128
505
  function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
129
506
  const wc = bundle.widgetConfig;
130
- const qtyFor = (productId, variantId) => (0, import_core4.resolveBundleQty)(bundle, productId, variantId);
507
+ const qtyFor = (productId, variantId) => (0, import_core5.resolveBundleQty)(bundle, productId, variantId);
131
508
  const rows = [];
132
509
  let oosCount = 0;
133
510
  bundle.products.forEach((product, idx) => {
@@ -192,11 +569,13 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
192
569
  })
193
570
  );
194
571
  container.appendChild(root);
572
+ bindAllDropdowns(root);
573
+ onCleanup?.(() => unbindAllDropdowns(root));
195
574
  updatePricing();
196
575
  function updatePricing() {
197
576
  const totalCents = rows.reduce((sum, r) => {
198
577
  if (!r.selected) return sum;
199
- const unit = (0, import_core.parseCents)(r.selected.price.amount);
578
+ const unit = (0, import_core2.parseCents)(r.selected.price.amount);
200
579
  return sum + unit * r.qty;
201
580
  }, 0);
202
581
  const saleCents = computeSale(totalCents, bundle.discountConfig, rows);
@@ -217,7 +596,7 @@ function buildRowState(bundle, product, productIndex) {
217
596
  const eligibleVariants = merchantScoped;
218
597
  const isOos = !firstInStock;
219
598
  const selected = firstInStock ?? merchantScoped[0] ?? null;
220
- const qty = selected ? (0, import_core4.resolveBundleQty)(bundle, product.id, selected.id) : 1;
599
+ const qty = selected ? (0, import_core5.resolveBundleQty)(bundle, product.id, selected.id) : 1;
221
600
  return { product, eligibleVariants, selected, qty, isOos };
222
601
  }
223
602
  function renderHeader(bundle, currency) {
@@ -237,7 +616,7 @@ function renderHeader(bundle, currency) {
237
616
  "data-header-badge": ""
238
617
  });
239
618
  header.appendChild(badgeEl);
240
- const initialPricing = (0, import_core.computeFixedPricing)(
619
+ const initialPricing = (0, import_core2.computeFixedPricing)(
241
620
  bundle,
242
621
  bundle.productQuantities,
243
622
  wc.pricing.showSaveBadge
@@ -273,9 +652,9 @@ function deriveHeaderBadge(bundle, totalCents, saleCents, currency) {
273
652
  return `-${Math.round(dc.discountValue)}%`;
274
653
  }
275
654
  if (dc.discountType === "fixed_amount" && dc.discountValue > 0) {
276
- return `-${(0, import_core.formatCents)(Math.round(dc.discountValue * 100), currency)}`;
655
+ return `-${(0, import_core2.formatCents)(Math.round(dc.discountValue * 100), currency)}`;
277
656
  }
278
- return `-${(0, import_core.formatCents)(savings, currency)}`;
657
+ return `-${(0, import_core2.formatCents)(savings, currency)}`;
279
658
  }
280
659
  function renderProductRow(state, currency, qtyFor, onVariantChange) {
281
660
  const rowEl = el(
@@ -287,18 +666,20 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
287
666
  }
288
667
  );
289
668
  const thumb = el("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
290
- if (state.product.featuredImage) {
291
- const img = document.createElement("img");
292
- img.src = (0, import_core3.transformImageUrl)(state.product.featuredImage.url, {
293
- width: import_core3.THUMB_PX,
294
- height: import_core3.THUMB_PX,
669
+ const initialThumbImage = state.selected?.image ?? state.product.featuredImage ?? null;
670
+ let thumbImg = null;
671
+ if (initialThumbImage) {
672
+ thumbImg = document.createElement("img");
673
+ thumbImg.src = (0, import_core4.transformImageUrl)(initialThumbImage.url, {
674
+ width: import_core4.THUMB_PX,
675
+ height: import_core4.THUMB_PX,
295
676
  crop: "center"
296
677
  });
297
- img.alt = state.product.featuredImage.altText ?? state.product.title;
298
- img.width = import_core3.THUMB_PX;
299
- img.height = import_core3.THUMB_PX;
300
- img.loading = "lazy";
301
- thumb.appendChild(img);
678
+ thumbImg.alt = initialThumbImage.altText ?? state.product.title;
679
+ thumbImg.width = import_core4.THUMB_PX;
680
+ thumbImg.height = import_core4.THUMB_PX;
681
+ thumbImg.loading = "lazy";
682
+ thumb.appendChild(thumbImg);
302
683
  } else {
303
684
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG);
304
685
  }
@@ -338,12 +719,12 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
338
719
  unitPriceEl.setAttribute("hidden", "");
339
720
  info.appendChild(unitPriceEl);
340
721
  const applyVariantToRow = (variant) => {
341
- const unit = (0, import_core.parseCents)(variant.price.amount);
342
- priceEl.textContent = (0, import_core.formatCents)(unit, currency);
722
+ const unit = (0, import_core2.parseCents)(variant.price.amount);
723
+ priceEl.textContent = (0, import_core2.formatCents)(unit, currency);
343
724
  if (variant.compareAtPrice) {
344
- const cmp = (0, import_core.parseCents)(variant.compareAtPrice.amount);
725
+ const cmp = (0, import_core2.parseCents)(variant.compareAtPrice.amount);
345
726
  if (cmp > unit) {
346
- compare.textContent = (0, import_core.formatCents)(cmp, currency);
727
+ compare.textContent = (0, import_core2.formatCents)(cmp, currency);
347
728
  compare.removeAttribute("hidden");
348
729
  } else {
349
730
  compare.setAttribute("hidden", "");
@@ -351,7 +732,7 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
351
732
  } else {
352
733
  compare.setAttribute("hidden", "");
353
734
  }
354
- const unitText = (0, import_core.formatUnitPrice)(
735
+ const unitText = (0, import_core2.formatUnitPrice)(
355
736
  variant.unitPrice,
356
737
  variant.unitPriceMeasurement,
357
738
  currency
@@ -362,6 +743,15 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
362
743
  } else {
363
744
  unitPriceEl.setAttribute("hidden", "");
364
745
  }
746
+ const nextImage = variant.image ?? state.product.featuredImage;
747
+ if (thumbImg && nextImage) {
748
+ thumbImg.src = (0, import_core4.transformImageUrl)(nextImage.url, {
749
+ width: import_core4.THUMB_PX,
750
+ height: import_core4.THUMB_PX,
751
+ crop: "center"
752
+ });
753
+ thumbImg.alt = nextImage.altText ?? state.product.title;
754
+ }
365
755
  };
366
756
  applyVariantToRow(state.selected);
367
757
  if (state.eligibleVariants.length > 1) {
@@ -472,9 +862,9 @@ function renderPricingRow(bundle) {
472
862
  return {
473
863
  el: row,
474
864
  update({ totalCents, saleCents, savingsCents, currency }) {
475
- sale.textContent = (0, import_core.formatCents)(saleCents, currency);
865
+ sale.textContent = (0, import_core2.formatCents)(saleCents, currency);
476
866
  if (bundle.widgetConfig.pricing.showCompareAtPrice && savingsCents > 0) {
477
- compare.textContent = (0, import_core.formatCents)(totalCents, currency);
867
+ compare.textContent = (0, import_core2.formatCents)(totalCents, currency);
478
868
  compare.style.display = "";
479
869
  } else {
480
870
  compare.style.display = "none";
@@ -499,7 +889,7 @@ function renderSavingsBar() {
499
889
  return;
500
890
  }
501
891
  bar.style.display = "";
502
- amount.textContent = (0, import_core.formatCents)(savingsCents, currency);
892
+ amount.textContent = (0, import_core2.formatCents)(savingsCents, currency);
503
893
  }
504
894
  };
505
895
  }
@@ -521,7 +911,7 @@ function computeSale(totalCents, discount, rows) {
521
911
  let saleCents = 0;
522
912
  for (const r of rows) {
523
913
  if (!r.selected) continue;
524
- const unit = (0, import_core.parseCents)(r.selected.price.amount);
914
+ const unit = (0, import_core2.parseCents)(r.selected.price.amount);
525
915
  const off = Math.floor(unit * discount.discountValue / 100);
526
916
  const perUnit = Math.max(0, unit - off);
527
917
  saleCents += perUnit * r.qty;
@@ -532,7 +922,7 @@ function computeSale(totalCents, discount, rows) {
532
922
  }
533
923
 
534
924
  // src/renderers/mix-match.ts
535
- var import_core5 = require("@lime-bundles/core");
925
+ var import_core6 = require("@lime-bundles/core");
536
926
  var PLACEHOLDER_THUMB_SVG2 = `
537
927
  <svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
538
928
  <rect x="4" y="4" width="20" height="20" rx="3"></rect>
@@ -628,6 +1018,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
628
1018
  })
629
1019
  );
630
1020
  container.appendChild(root);
1021
+ onCleanup?.(() => unbindAllDropdowns(root));
631
1022
  const firstEligible = eligible.find((ep) => !ep.isOos);
632
1023
  const firstVariant = firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];
633
1024
  if (firstEligible && firstVariant) {
@@ -636,15 +1027,15 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
636
1027
  productTitle: firstEligible.product.title,
637
1028
  variantId: firstVariant.id,
638
1029
  variantTitle: firstVariant.title,
639
- imageUrl: firstEligible.product.featuredImage?.url ?? null,
640
- priceCents: (0, import_core.parseCents)(firstVariant.price.amount),
641
- compareCents: firstVariant.compareAtPrice ? (0, import_core.parseCents)(firstVariant.compareAtPrice.amount) : null,
642
- unitPriceLabel: (0, import_core.formatUnitPrice)(
1030
+ imageUrl: firstVariant.image?.url ?? firstEligible.product.featuredImage?.url ?? null,
1031
+ priceCents: (0, import_core2.parseCents)(firstVariant.price.amount),
1032
+ compareCents: firstVariant.compareAtPrice ? (0, import_core2.parseCents)(firstVariant.compareAtPrice.amount) : null,
1033
+ unitPriceLabel: (0, import_core2.formatUnitPrice)(
643
1034
  firstVariant.unitPrice,
644
1035
  firstVariant.unitPriceMeasurement,
645
1036
  currency
646
1037
  ),
647
- quantity: (0, import_core5.resolveBundleQty)(bundle, firstEligible.product.id, firstVariant.id)
1038
+ quantity: (0, import_core6.resolveBundleQty)(bundle, firstEligible.product.id, firstVariant.id)
648
1039
  });
649
1040
  }
650
1041
  afterMutation();
@@ -655,15 +1046,15 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
655
1046
  productTitle: product.title,
656
1047
  variantId: variant.id,
657
1048
  variantTitle: variant.title,
658
- imageUrl: product.featuredImage?.url ?? null,
659
- priceCents: (0, import_core.parseCents)(variant.price.amount),
660
- compareCents: variant.compareAtPrice ? (0, import_core.parseCents)(variant.compareAtPrice.amount) : null,
661
- unitPriceLabel: (0, import_core.formatUnitPrice)(
1049
+ imageUrl: variant.image?.url ?? product.featuredImage?.url ?? null,
1050
+ priceCents: (0, import_core2.parseCents)(variant.price.amount),
1051
+ compareCents: variant.compareAtPrice ? (0, import_core2.parseCents)(variant.compareAtPrice.amount) : null,
1052
+ unitPriceLabel: (0, import_core2.formatUnitPrice)(
662
1053
  variant.unitPrice,
663
1054
  variant.unitPriceMeasurement,
664
1055
  currency
665
1056
  ),
666
- quantity: (0, import_core5.resolveBundleQty)(bundle, product.id, variant.id)
1057
+ quantity: (0, import_core6.resolveBundleQty)(bundle, product.id, variant.id)
667
1058
  });
668
1059
  afterMutation();
669
1060
  }
@@ -730,7 +1121,7 @@ function renderHeader2(bundle) {
730
1121
  if (discountType === "percentage" && discountValue > 0) {
731
1122
  label = `-${Math.round(discountValue)}%`;
732
1123
  } else if (discountType === "fixed_amount" && discountValue > 0) {
733
- label = `-${(0, import_core.formatCents)(
1124
+ label = `-${(0, import_core2.formatCents)(
734
1125
  Math.round(discountValue * 100),
735
1126
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
736
1127
  )}`;
@@ -817,14 +1208,14 @@ function renderFilledSlot(selection, index, currency, onRemove) {
817
1208
  const thumb = el("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
818
1209
  if (selection.imageUrl) {
819
1210
  const img = document.createElement("img");
820
- img.src = (0, import_core3.transformImageUrl)(selection.imageUrl, {
821
- width: import_core3.THUMB_PX,
822
- height: import_core3.THUMB_PX,
1211
+ img.src = (0, import_core4.transformImageUrl)(selection.imageUrl, {
1212
+ width: import_core4.THUMB_PX,
1213
+ height: import_core4.THUMB_PX,
823
1214
  crop: "center"
824
1215
  });
825
1216
  img.alt = selection.productTitle;
826
- img.width = import_core3.THUMB_PX;
827
- img.height = import_core3.THUMB_PX;
1217
+ img.width = import_core4.THUMB_PX;
1218
+ img.height = import_core4.THUMB_PX;
828
1219
  img.loading = "lazy";
829
1220
  thumb.appendChild(img);
830
1221
  } else {
@@ -848,11 +1239,11 @@ function renderFilledSlot(selection, index, currency, onRemove) {
848
1239
  const priceWrap = el("span", "lb-mix-match__filled-price");
849
1240
  if (lineCompare !== null && lineCompare > linePrice) {
850
1241
  const compare = el("span", "lb-mix-match__filled-compare");
851
- compare.textContent = (0, import_core.formatCents)(lineCompare, currency);
1242
+ compare.textContent = (0, import_core2.formatCents)(lineCompare, currency);
852
1243
  priceWrap.appendChild(compare);
853
1244
  }
854
1245
  const priceEl = document.createElement("span");
855
- priceEl.textContent = (0, import_core.formatCents)(linePrice, currency);
1246
+ priceEl.textContent = (0, import_core2.formatCents)(linePrice, currency);
856
1247
  priceWrap.appendChild(priceEl);
857
1248
  info.appendChild(priceWrap);
858
1249
  if (selection.unitPriceLabel) {
@@ -897,14 +1288,14 @@ function renderPricingSection(showCompareAtPrice) {
897
1288
  (s, sel) => s + sel.priceCents * sel.quantity,
898
1289
  0
899
1290
  );
900
- const saleCents = (0, import_core.computeBundleSaleCents)(totalCents, bundle.discountConfig);
1291
+ const saleCents = (0, import_core2.computeBundleSaleCents)(totalCents, bundle.discountConfig);
901
1292
  if (showCompareAtPrice && totalCents > saleCents) {
902
- compare.textContent = (0, import_core.formatCents)(totalCents, currency);
1293
+ compare.textContent = (0, import_core2.formatCents)(totalCents, currency);
903
1294
  compare.style.display = "";
904
1295
  } else {
905
1296
  compare.style.display = "none";
906
1297
  }
907
- sale.textContent = (0, import_core.formatCents)(saleCents, currency);
1298
+ sale.textContent = (0, import_core2.formatCents)(saleCents, currency);
908
1299
  }
909
1300
  return { el: wrap, update };
910
1301
  }
@@ -927,14 +1318,14 @@ function renderSavingsBar2() {
927
1318
  (s, sel) => s + sel.priceCents * sel.quantity,
928
1319
  0
929
1320
  );
930
- const saleCents = (0, import_core.computeBundleSaleCents)(totalCents, bundle.discountConfig);
1321
+ const saleCents = (0, import_core2.computeBundleSaleCents)(totalCents, bundle.discountConfig);
931
1322
  const savings = Math.max(0, totalCents - saleCents);
932
1323
  if (savings <= 0) {
933
1324
  wrap.style.display = "none";
934
1325
  return;
935
1326
  }
936
1327
  wrap.style.display = "";
937
- amount.textContent = (0, import_core.formatCents)(savings, currency);
1328
+ amount.textContent = (0, import_core2.formatCents)(savings, currency);
938
1329
  }
939
1330
  return { el: wrap, update };
940
1331
  }
@@ -1030,24 +1421,27 @@ function renderModal(bundle, eligible, currency, handlers) {
1030
1421
  { "data-product-id": ep.product.id.replace(/^.*\//, "") }
1031
1422
  );
1032
1423
  const thumb = el("div", "lb-mix-match__modal-product-thumb");
1033
- if (ep.product.featuredImage) {
1034
- const img = document.createElement("img");
1035
- img.src = (0, import_core3.transformImageUrl)(ep.product.featuredImage.url, {
1036
- width: import_core3.THUMB_PX,
1037
- height: import_core3.THUMB_PX,
1424
+ const initialThumbVariant = ep.variants.find((v) => v.availableForSale) ?? ep.variants[0] ?? null;
1425
+ const initialThumbImage = initialThumbVariant?.image ?? ep.product.featuredImage ?? null;
1426
+ let thumbImg = null;
1427
+ if (initialThumbImage) {
1428
+ thumbImg = document.createElement("img");
1429
+ thumbImg.src = (0, import_core4.transformImageUrl)(initialThumbImage.url, {
1430
+ width: import_core4.THUMB_PX,
1431
+ height: import_core4.THUMB_PX,
1038
1432
  crop: "center"
1039
1433
  });
1040
- img.alt = ep.product.featuredImage.altText ?? ep.product.title;
1041
- img.width = import_core3.THUMB_PX;
1042
- img.height = import_core3.THUMB_PX;
1043
- img.loading = "lazy";
1044
- thumb.appendChild(img);
1434
+ thumbImg.alt = initialThumbImage.altText ?? ep.product.title;
1435
+ thumbImg.width = import_core4.THUMB_PX;
1436
+ thumbImg.height = import_core4.THUMB_PX;
1437
+ thumbImg.loading = "lazy";
1438
+ thumb.appendChild(thumbImg);
1045
1439
  } else {
1046
1440
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
1047
1441
  }
1048
1442
  const countBadge = el("span", "lb-bundle-qty-badge");
1049
1443
  countBadge.textContent = String(
1050
- (0, import_core5.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1444
+ (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1051
1445
  );
1052
1446
  thumb.appendChild(countBadge);
1053
1447
  productEl.appendChild(thumb);
@@ -1056,8 +1450,8 @@ function renderModal(bundle, eligible, currency, handlers) {
1056
1450
  title.textContent = ep.product.title;
1057
1451
  info.appendChild(title);
1058
1452
  const price = el("p", "lb-mix-match__modal-product-price");
1059
- price.textContent = (0, import_core.formatCents)(
1060
- (0, import_core.parseCents)(currentVariant.price.amount) * (0, import_core5.resolveBundleQty)(bundle, ep.product.id, currentVariant.id),
1453
+ price.textContent = (0, import_core2.formatCents)(
1454
+ (0, import_core2.parseCents)(currentVariant.price.amount) * (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id),
1061
1455
  currency
1062
1456
  );
1063
1457
  info.appendChild(price);
@@ -1065,7 +1459,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1065
1459
  "p",
1066
1460
  "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price"
1067
1461
  );
1068
- const initialUnitText = (0, import_core.formatUnitPrice)(
1462
+ const initialUnitText = (0, import_core2.formatUnitPrice)(
1069
1463
  currentVariant.unitPrice,
1070
1464
  currentVariant.unitPriceMeasurement,
1071
1465
  currency
@@ -1124,16 +1518,16 @@ function renderModal(bundle, eligible, currency, handlers) {
1124
1518
  }
1125
1519
  currentVariant = next;
1126
1520
  row.variant = next;
1127
- const nextQty = (0, import_core5.resolveBundleQty)(
1521
+ const nextQty = (0, import_core6.resolveBundleQty)(
1128
1522
  bundle,
1129
1523
  ep.product.id,
1130
1524
  currentVariant.id
1131
1525
  );
1132
- price.textContent = (0, import_core.formatCents)(
1133
- (0, import_core.parseCents)(currentVariant.price.amount) * nextQty,
1526
+ price.textContent = (0, import_core2.formatCents)(
1527
+ (0, import_core2.parseCents)(currentVariant.price.amount) * nextQty,
1134
1528
  currency
1135
1529
  );
1136
- const nextUnitText = (0, import_core.formatUnitPrice)(
1530
+ const nextUnitText = (0, import_core2.formatUnitPrice)(
1137
1531
  currentVariant.unitPrice,
1138
1532
  currentVariant.unitPriceMeasurement,
1139
1533
  currency
@@ -1145,6 +1539,15 @@ function renderModal(bundle, eligible, currency, handlers) {
1145
1539
  unitPrice.textContent = "";
1146
1540
  unitPrice.hidden = true;
1147
1541
  }
1542
+ const nextImage = currentVariant.image ?? ep.product.featuredImage;
1543
+ if (thumbImg && nextImage) {
1544
+ thumbImg.src = (0, import_core4.transformImageUrl)(nextImage.url, {
1545
+ width: import_core4.THUMB_PX,
1546
+ height: import_core4.THUMB_PX,
1547
+ crop: "center"
1548
+ });
1549
+ thumbImg.alt = nextImage.altText ?? ep.product.title;
1550
+ }
1148
1551
  recomputeDisabled(next.selectedOptions.map((o) => o.value));
1149
1552
  rowUpdateCount();
1150
1553
  };
@@ -1195,7 +1598,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1195
1598
  productEl.appendChild(info);
1196
1599
  const rowUpdateCount = () => {
1197
1600
  countBadge.textContent = String(
1198
- (0, import_core5.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1601
+ (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1199
1602
  );
1200
1603
  };
1201
1604
  if (!ep.isOos) {
@@ -1215,6 +1618,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1215
1618
  productRows.push(row);
1216
1619
  list.appendChild(productEl);
1217
1620
  });
1621
+ bindAllDropdowns(list);
1218
1622
  refreshCounts();
1219
1623
  }
1220
1624
  function applySearch() {
@@ -1318,7 +1722,7 @@ function renderVolumeBundle(container, bundle, onAddToCart, onCleanup) {
1318
1722
  const product = bundle.products[0];
1319
1723
  const variant = product?.variants.nodes.find((v) => v.availableForSale);
1320
1724
  if (!variant && wc.outOfStockBehavior === "hide") return;
1321
- const basePriceCents = variant ? (0, import_core.parseCents)(variant.price.amount) : 0;
1725
+ const basePriceCents = variant ? (0, import_core2.parseCents)(variant.price.amount) : 0;
1322
1726
  const currency = variant?.price.currencyCode ?? "USD";
1323
1727
  const discountType = bundle.discountConfig.discountType;
1324
1728
  const resolved = bundle.volumeTiers.map((tier, index) => {
@@ -1497,13 +1901,13 @@ function renderTierCard(r, isSelected, currency, popularLabel, showComparePrice,
1497
1901
  const price = el("span", "lb-volume__tier-price");
1498
1902
  if (showComparePrice && r.pricePerUnitCents < r.basePricePerUnitCents) {
1499
1903
  const compare = el("span", "lb-volume__tier-compare");
1500
- compare.textContent = (0, import_core.formatCents)(r.basePricePerUnitCents, currency);
1904
+ compare.textContent = (0, import_core2.formatCents)(r.basePricePerUnitCents, currency);
1501
1905
  price.appendChild(compare);
1502
1906
  }
1503
1907
  if (showPerUnitPrice) {
1504
1908
  const each = document.createElement("span");
1505
1909
  each.setAttribute("data-tier-price-each", "");
1506
- each.textContent = (0, import_core.formatCents)(r.pricePerUnitCents, currency);
1910
+ each.textContent = (0, import_core2.formatCents)(r.pricePerUnitCents, currency);
1507
1911
  price.appendChild(each);
1508
1912
  const unit = el("span", "lb-volume__tier-unit");
1509
1913
  unit.textContent = " each";
@@ -1542,11 +1946,11 @@ function renderPricingRow2(resolved, selectedIndex, currency, showItemCount, sho
1542
1946
  const compare = el("span", "lb-bundle-compare-price", {
1543
1947
  "data-compare-price": ""
1544
1948
  });
1545
- compare.textContent = (0, import_core.formatCents)(undiscountedCents, currency);
1949
+ compare.textContent = (0, import_core2.formatCents)(undiscountedCents, currency);
1546
1950
  prices.appendChild(compare);
1547
1951
  }
1548
1952
  const sale = el("span", "lb-bundle-sale-price", { "data-total-price": "" });
1549
- sale.textContent = (0, import_core.formatCents)(totalCents, currency);
1953
+ sale.textContent = (0, import_core2.formatCents)(totalCents, currency);
1550
1954
  prices.appendChild(sale);
1551
1955
  row.appendChild(prices);
1552
1956
  return row;
@@ -1562,7 +1966,7 @@ function renderSavingsBar3(resolved, selectedIndex, currency) {
1562
1966
  labelEl.textContent = "You save";
1563
1967
  bar.appendChild(labelEl);
1564
1968
  const amount = el("span", "", { "data-savings-amount": "" });
1565
- amount.textContent = (0, import_core.formatCents)(savings, currency);
1969
+ amount.textContent = (0, import_core2.formatCents)(savings, currency);
1566
1970
  bar.appendChild(amount);
1567
1971
  return bar;
1568
1972
  }
@@ -1583,7 +1987,7 @@ function badgeFor(resolved, currency, discountType) {
1583
1987
  const { tier } = resolved;
1584
1988
  if (discountType === "fixed_amount") {
1585
1989
  const amount = tier.amount ?? 0;
1586
- if (amount > 0) return `-${(0, import_core.formatCents)(Math.round(amount * 100), currency)}`;
1990
+ if (amount > 0) return `-${(0, import_core2.formatCents)(Math.round(amount * 100), currency)}`;
1587
1991
  return null;
1588
1992
  }
1589
1993
  if (discountType === "percentage") {
@@ -1610,7 +2014,7 @@ function clamp(n, min, max) {
1610
2014
  }
1611
2015
 
1612
2016
  // src/lime-bundle.ts
1613
- var import_core7 = require("@lime-bundles/core");
2017
+ var import_core8 = require("@lime-bundles/core");
1614
2018
 
1615
2019
  // src/utils/input-mode.ts
1616
2020
  var NAV_KEYS = /* @__PURE__ */ new Set([
@@ -1680,6 +2084,11 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1680
2084
  --lb-thumbnail-bg: #F0F0F0;
1681
2085
  --lb-widget-pad: 20px;
1682
2086
  --lb-progress-color: var(--lb-primary-color);
2087
+ /* Cap on the per-bundle product/slot/tier list height \u2014 keeps long
2088
+ bundles from pushing the CTA off-screen. The list scrolls
2089
+ internally with the same custom 4px scrollbar as the variant
2090
+ dropdown when content exceeds this. */
2091
+ --lb-list-max-height: 360px;
1683
2092
 
1684
2093
  font-family: inherit;
1685
2094
  font-size: 16px;
@@ -2234,6 +2643,22 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2234
2643
  flex-direction: column;
2235
2644
  gap: 0;
2236
2645
  margin: 0;
2646
+ max-height: var(--lb-list-max-height);
2647
+ overflow-y: auto;
2648
+ /* Custom scrollbar \u2014 text colour at 15% opacity (thumb) and 5% (track). */
2649
+ scrollbar-width: thin;
2650
+ scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)
2651
+ color-mix(in srgb, var(--lb-text) 2%, transparent);
2652
+ }
2653
+
2654
+ .lb-fixed__products::-webkit-scrollbar { width: 4px; }
2655
+ .lb-fixed__products::-webkit-scrollbar-track {
2656
+ background: color-mix(in srgb, var(--lb-text) 2%, transparent);
2657
+ border-radius: 2px;
2658
+ }
2659
+ .lb-fixed__products::-webkit-scrollbar-thumb {
2660
+ background: color-mix(in srgb, var(--lb-text) 15%, transparent);
2661
+ border-radius: 2px;
2237
2662
  }
2238
2663
 
2239
2664
  /* Fixed bundles: product rows */
@@ -2289,6 +2714,28 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2289
2714
  `;
2290
2715
  var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2291
2716
 
2717
+ /* === Slot list ============================================================
2718
+ Caps the height of the slot stack so long bundles don't push the CTA off
2719
+ the page. Internal scroll with the same custom 4px scrollbar as the
2720
+ variant dropdown panel. */
2721
+ .lb-mix-match__slots {
2722
+ max-height: var(--lb-list-max-height);
2723
+ overflow-y: auto;
2724
+ scrollbar-width: thin;
2725
+ scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)
2726
+ color-mix(in srgb, var(--lb-text) 2%, transparent);
2727
+ }
2728
+
2729
+ .lb-mix-match__slots::-webkit-scrollbar { width: 4px; }
2730
+ .lb-mix-match__slots::-webkit-scrollbar-track {
2731
+ background: color-mix(in srgb, var(--lb-text) 2%, transparent);
2732
+ border-radius: 2px;
2733
+ }
2734
+ .lb-mix-match__slots::-webkit-scrollbar-thumb {
2735
+ background: color-mix(in srgb, var(--lb-text) 15%, transparent);
2736
+ border-radius: 2px;
2737
+ }
2738
+
2292
2739
  /* === Progress Bar === */
2293
2740
  .lb-mix-match__progress {
2294
2741
  margin-bottom: 16px;
@@ -2805,6 +3252,22 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2805
3252
  display: flex;
2806
3253
  flex-direction: column;
2807
3254
  gap: 12px;
3255
+ max-height: var(--lb-list-max-height);
3256
+ overflow-y: auto;
3257
+ /* Custom scrollbar \u2014 text colour at 15% opacity (thumb) and 5% (track). */
3258
+ scrollbar-width: thin;
3259
+ scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)
3260
+ color-mix(in srgb, var(--lb-text) 2%, transparent);
3261
+ }
3262
+
3263
+ .lb-volume__tiers::-webkit-scrollbar { width: 4px; }
3264
+ .lb-volume__tiers::-webkit-scrollbar-track {
3265
+ background: color-mix(in srgb, var(--lb-text) 2%, transparent);
3266
+ border-radius: 2px;
3267
+ }
3268
+ .lb-volume__tiers::-webkit-scrollbar-thumb {
3269
+ background: color-mix(in srgb, var(--lb-text) 15%, transparent);
3270
+ border-radius: 2px;
2808
3271
  }
2809
3272
 
2810
3273
  .lb-volume__tier {
@@ -2909,6 +3372,259 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2909
3372
  }
2910
3373
 
2911
3374
 
3375
+ `;
3376
+ var BUNDLE_DROPDOWN_CSS = `/**
3377
+ * Lime Bundles \u2014 Custom variant-picker dropdown styling.
3378
+ *
3379
+ * Reuses existing CSS variables: no new merchant-configurable surface.
3380
+ * --lb-variant-border-{width,color}, --lb-variant-radius, --lb-variant-chevron
3381
+ * --lb-bg, --lb-text, --lb-primary-color
3382
+ *
3383
+ * Mix-match modal context overrides via .lb-mix-match__modal scope to use
3384
+ * --lb-picker-variant-* and --lb-picker-bg.
3385
+ */
3386
+
3387
+ /* Hide the native <select> while keeping it form-serializable and focusable
3388
+ programmatically. The .lb-dropdown-state marker is added by JS at bind
3389
+ time, so this rule matches every variant-select class (main widget,
3390
+ mix-match modal, future bundle types). aria-hidden + tabindex=-1
3391
+ (also set in JS) remove it from the accessibility tree. */
3392
+ .lb-dropdown-state {
3393
+ position: absolute !important;
3394
+ width: 1px !important;
3395
+ height: 1px !important;
3396
+ padding: 0 !important;
3397
+ margin: -1px !important;
3398
+ overflow: hidden !important;
3399
+ clip: rect(0 0 0 0) !important;
3400
+ white-space: nowrap !important;
3401
+ border: 0 !important;
3402
+ pointer-events: none !important;
3403
+ }
3404
+
3405
+ /* Shell fills its parent column. */
3406
+ .lb-dropdown {
3407
+ position: relative;
3408
+ display: inline-block;
3409
+ width: 100%;
3410
+ max-width: 100%;
3411
+ font-family: inherit;
3412
+ }
3413
+
3414
+ /* Trigger styled identically to the closed-state native select */
3415
+ .lb-dropdown-trigger {
3416
+ display: inline-flex;
3417
+ align-items: center;
3418
+ justify-content: space-between;
3419
+ gap: 8px;
3420
+ width: 100%;
3421
+ border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
3422
+ border-radius: var(--lb-variant-radius);
3423
+ padding: 8px 12px;
3424
+ font-size: 12px;
3425
+ line-height: 16px;
3426
+ color: var(--lb-text);
3427
+ background: var(--lb-bg);
3428
+ font-family: inherit;
3429
+ cursor: pointer;
3430
+ appearance: none;
3431
+ -webkit-appearance: none;
3432
+ text-align: start;
3433
+ transition: border-color 120ms ease;
3434
+ }
3435
+
3436
+ .lb-dropdown-trigger:focus-visible {
3437
+ outline: 2px solid var(--lb-primary-color);
3438
+ outline-offset: 2px;
3439
+ }
3440
+
3441
+ .lb-dropdown-trigger[aria-expanded="true"] {
3442
+ border-color: var(--lb-text);
3443
+ }
3444
+
3445
+ .lb-dropdown-trigger-value {
3446
+ flex: 1 1 auto;
3447
+ white-space: nowrap;
3448
+ overflow: hidden;
3449
+ text-overflow: ellipsis;
3450
+ text-align: start;
3451
+ }
3452
+
3453
+ .lb-dropdown-chevron {
3454
+ flex: 0 0 auto;
3455
+ width: 12px;
3456
+ height: 12px;
3457
+ background: var(--lb-variant-chevron) center / contain no-repeat;
3458
+ transition: transform 120ms ease;
3459
+ }
3460
+
3461
+ .lb-dropdown-trigger[aria-expanded="true"] .lb-dropdown-chevron {
3462
+ transform: rotate(180deg);
3463
+ }
3464
+
3465
+ /* Popover panel \u2014 position: absolute against the .lb-dropdown shell
3466
+ (already position: relative). Top/left/width come from CSS so we
3467
+ never depend on JS having set inline coords by the time the panel
3468
+ becomes visible. JS only sets max-height. */
3469
+ .lb-dropdown-listbox {
3470
+ position: absolute;
3471
+ left: 0;
3472
+ /* Default to below-trigger placement so the panel doesn't overlap the
3473
+ trigger if data-placement is missing for any reason. The explicit
3474
+ [data-placement="down"|"up"] rules below override this. */
3475
+ top: calc(100% + 4px);
3476
+ width: 100%;
3477
+ z-index: 9999;
3478
+ margin: 0;
3479
+ padding: 4px 0;
3480
+ list-style: none;
3481
+ background: var(--lb-bg);
3482
+ color: var(--lb-text);
3483
+ border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
3484
+ border-radius: var(--lb-variant-radius);
3485
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
3486
+ overflow-y: auto;
3487
+ overflow-x: hidden;
3488
+ /* Custom scrollbar \u2014 text colour at 15% opacity (thumb) and 5% (track). */
3489
+ scrollbar-width: thin;
3490
+ scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)
3491
+ color-mix(in srgb, var(--lb-text) 2%, transparent);
3492
+ animation: lb-dropdown-in-down 120ms ease-out;
3493
+ transform-origin: top center;
3494
+ }
3495
+
3496
+ .lb-dropdown-listbox[data-placement="down"] {
3497
+ top: calc(100% + 4px);
3498
+ }
3499
+
3500
+ .lb-dropdown-listbox[data-placement="up"] {
3501
+ top: auto;
3502
+ bottom: calc(100% + 4px);
3503
+ animation-name: lb-dropdown-in-up;
3504
+ transform-origin: bottom center;
3505
+ }
3506
+
3507
+ /* When portaled out of the .lb-dropdown shell (mix-match modal context:
3508
+ .lb-mix-match__modal applies translateY which would otherwise trap
3509
+ position:fixed), switch to fixed and let JS set viewport coords. */
3510
+ .lb-dropdown-listbox[data-lb-dropdown-portal] {
3511
+ position: fixed;
3512
+ top: auto;
3513
+ left: auto;
3514
+ bottom: auto;
3515
+ width: auto;
3516
+ }
3517
+
3518
+ /* Custom scrollbar \u2014 Webkit/Blink: exact 4px width */
3519
+ .lb-dropdown-listbox::-webkit-scrollbar {
3520
+ width: 4px;
3521
+ }
3522
+ .lb-dropdown-listbox::-webkit-scrollbar-track {
3523
+ background: color-mix(in srgb, var(--lb-text) 2%, transparent);
3524
+ border-radius: 2px;
3525
+ }
3526
+ .lb-dropdown-listbox::-webkit-scrollbar-thumb {
3527
+ background: color-mix(in srgb, var(--lb-text) 15%, transparent);
3528
+ border-radius: 2px;
3529
+ }
3530
+
3531
+ /* Options */
3532
+ .lb-dropdown-option {
3533
+ padding: 8px 12px;
3534
+ font-size: 12px;
3535
+ line-height: 16px;
3536
+ cursor: pointer;
3537
+ white-space: nowrap;
3538
+ overflow: hidden;
3539
+ text-overflow: ellipsis;
3540
+ color: var(--lb-text);
3541
+ }
3542
+
3543
+ .lb-dropdown-option[aria-selected="true"] {
3544
+ font-weight: 600;
3545
+ }
3546
+
3547
+ .lb-dropdown-option.is-active,
3548
+ .lb-dropdown-option:hover:not([aria-disabled="true"]) {
3549
+ background: color-mix(in srgb, var(--lb-text) 2%, transparent);
3550
+ }
3551
+
3552
+ .lb-dropdown-option[aria-disabled="true"] {
3553
+ opacity: 0.4;
3554
+ cursor: not-allowed;
3555
+ }
3556
+
3557
+ /* Animations */
3558
+ @keyframes lb-dropdown-in-down {
3559
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
3560
+ to { opacity: 1; transform: translateY(0) scale(1); }
3561
+ }
3562
+
3563
+ @keyframes lb-dropdown-in-up {
3564
+ from { opacity: 0; transform: translateY(4px) scale(0.98); }
3565
+ to { opacity: 1; transform: translateY(0) scale(1); }
3566
+ }
3567
+
3568
+ @media (prefers-reduced-motion: reduce) {
3569
+ .lb-dropdown-listbox { animation: none; }
3570
+ .lb-dropdown-chevron { transition: none; }
3571
+ .lb-dropdown-trigger { transition: none; }
3572
+ }
3573
+
3574
+ /* Forced-colors mode (Windows high-contrast) */
3575
+ @media (forced-colors: active) {
3576
+ .lb-dropdown-trigger {
3577
+ border-color: ButtonBorder;
3578
+ color: ButtonText;
3579
+ background: ButtonFace;
3580
+ }
3581
+ .lb-dropdown-listbox {
3582
+ border-color: ButtonBorder;
3583
+ background: Canvas;
3584
+ color: CanvasText;
3585
+ }
3586
+ .lb-dropdown-option.is-active {
3587
+ background: Highlight;
3588
+ color: HighlightText;
3589
+ }
3590
+ }
3591
+
3592
+ /* Mix-match modal context \u2014 use picker-scoped variables.
3593
+ No CSS fallbacks: --lb-picker-* are always emitted by bundle-widget.liquid
3594
+ because WidgetConfig.parse() fully hydrates the merchant config.
3595
+ See docs/solutions/ui-bugs/widget-css-single-source-defaults.md. */
3596
+ .lb-mix-match__modal .lb-dropdown-trigger {
3597
+ border-color: var(--lb-picker-variant-border-color);
3598
+ border-width: var(--lb-picker-variant-border-width);
3599
+ border-radius: var(--lb-picker-variant-radius);
3600
+ background: var(--lb-picker-bg);
3601
+ color: var(--lb-picker-text);
3602
+ }
3603
+
3604
+ .lb-mix-match__modal .lb-dropdown-chevron {
3605
+ background-image: var(--lb-picker-variant-chevron);
3606
+ }
3607
+
3608
+ /* Listbox is portaled out of the transformed .lb-mix-match__modal up to
3609
+ its [data-modal-overlay] parent, so picker-scoped rules anchor on the
3610
+ overlay attribute, not the modal class. */
3611
+ [data-modal-overlay] > .lb-dropdown-listbox {
3612
+ border-color: var(--lb-picker-variant-border-color);
3613
+ border-width: var(--lb-picker-variant-border-width);
3614
+ border-radius: var(--lb-picker-variant-radius);
3615
+ background: var(--lb-picker-bg);
3616
+ color: var(--lb-picker-text);
3617
+ scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)
3618
+ color-mix(in srgb, var(--lb-picker-text) 2%, transparent);
3619
+ }
3620
+
3621
+ [data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {
3622
+ background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);
3623
+ border-radius: 2px;
3624
+ }
3625
+ [data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-thumb {
3626
+ background: color-mix(in srgb, var(--lb-picker-text) 15%, transparent);
3627
+ }
2912
3628
  `;
2913
3629
  var BUNDLE_SKELETON_CSS = `/* Lime Bundles \u2014 web-component loading skeleton (not mirrored to theme assets) */
2914
3630
 
@@ -3107,7 +3823,7 @@ var LimeBundleElement = class extends HTMLElement {
3107
3823
  const controller = new AbortController();
3108
3824
  this.abortController = controller;
3109
3825
  this.renderLoading();
3110
- const client = (0, import_core6.createStorefrontClient)({
3826
+ const client = (0, import_core7.createStorefrontClient)({
3111
3827
  shopDomain: this.shopDomain,
3112
3828
  accessToken: this.storefrontToken
3113
3829
  });
@@ -3132,7 +3848,7 @@ var LimeBundleElement = class extends HTMLElement {
3132
3848
  handle
3133
3849
  );
3134
3850
  }
3135
- const cssPromise = client.query(import_core6.SHOP_CUSTOM_CSS_QUERY, void 0, {
3851
+ const cssPromise = client.query(import_core7.SHOP_CUSTOM_CSS_QUERY, void 0, {
3136
3852
  signal: controller.signal
3137
3853
  }).catch(() => null);
3138
3854
  await bundlePromise;
@@ -3144,8 +3860,8 @@ var LimeBundleElement = class extends HTMLElement {
3144
3860
  }
3145
3861
  const css = await cssPromise;
3146
3862
  if (css?.shop?.metafield?.value) {
3147
- (0, import_core6.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
3148
- const sanitized = (0, import_core6.sanitizeCustomCss)(css.shop.metafield.value);
3863
+ (0, import_core7.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
3864
+ const sanitized = (0, import_core7.sanitizeCustomCss)(css.shop.metafield.value);
3149
3865
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
3150
3866
  }
3151
3867
  await this.applyABVariants();
@@ -3173,14 +3889,14 @@ var LimeBundleElement = class extends HTMLElement {
3173
3889
  this.bundles.map(async (bundle) => {
3174
3890
  if (!bundle.abTestId || !bundle.abVariantB) return bundle;
3175
3891
  try {
3176
- const assignment = await (0, import_core6.getABTestAssignment)(
3892
+ const assignment = await (0, import_core7.getABTestAssignment)(
3177
3893
  this.appUrl,
3178
3894
  this.shopDomain,
3179
3895
  bundle.abTestId,
3180
3896
  bundle.id
3181
3897
  );
3182
3898
  if (assignment?.variant === "B") {
3183
- return (0, import_core6.applyABVariantB)(bundle);
3899
+ return (0, import_core7.applyABVariantB)(bundle);
3184
3900
  }
3185
3901
  } catch (err) {
3186
3902
  console.warn(
@@ -3195,7 +3911,7 @@ var LimeBundleElement = class extends HTMLElement {
3195
3911
  }
3196
3912
  async fetchSingleBundle(client, signal) {
3197
3913
  const data = await client.query(
3198
- import_core6.BUNDLE_METAOBJECT_QUERY,
3914
+ import_core7.BUNDLE_METAOBJECT_QUERY,
3199
3915
  { id: this.bundleGid },
3200
3916
  { signal }
3201
3917
  );
@@ -3203,7 +3919,7 @@ var LimeBundleElement = class extends HTMLElement {
3203
3919
  this.bundles = [];
3204
3920
  return;
3205
3921
  }
3206
- const parsed = (0, import_core6.parseMetaobjectBundle)(
3922
+ const parsed = (0, import_core7.parseMetaobjectBundle)(
3207
3923
  data.metaobject.id,
3208
3924
  data.metaobject.fields
3209
3925
  );
@@ -3211,7 +3927,7 @@ var LimeBundleElement = class extends HTMLElement {
3211
3927
  }
3212
3928
  async fetchProductBundles(client, signal, productHandle) {
3213
3929
  const data = await client.query(
3214
- import_core6.BUNDLES_FOR_PRODUCT_QUERY,
3930
+ import_core7.BUNDLES_FOR_PRODUCT_QUERY,
3215
3931
  { handle: productHandle },
3216
3932
  { signal }
3217
3933
  );
@@ -3222,7 +3938,7 @@ var LimeBundleElement = class extends HTMLElement {
3222
3938
  const refs = data.product.metafield?.references?.nodes ?? [];
3223
3939
  const bundles = [];
3224
3940
  for (const ref of refs) {
3225
- const parsed = (0, import_core6.parseMetaobjectBundle)(ref.id, ref.fields);
3941
+ const parsed = (0, import_core7.parseMetaobjectBundle)(ref.id, ref.fields);
3226
3942
  if (parsed) bundles.push(parsed);
3227
3943
  }
3228
3944
  this.bundles = bundles;
@@ -3255,7 +3971,7 @@ var LimeBundleElement = class extends HTMLElement {
3255
3971
  */
3256
3972
  async defaultAddToCart(lines) {
3257
3973
  if (typeof window === "undefined") return;
3258
- const client = (0, import_core6.createStorefrontClient)({
3974
+ const client = (0, import_core7.createStorefrontClient)({
3259
3975
  shopDomain: this.shopDomain,
3260
3976
  accessToken: this.storefrontToken
3261
3977
  });
@@ -3266,7 +3982,7 @@ var LimeBundleElement = class extends HTMLElement {
3266
3982
  let checkoutUrl = null;
3267
3983
  if (existingCartId) {
3268
3984
  const res = await client.query(
3269
- import_core6.CART_LINES_ADD_MUTATION,
3985
+ import_core7.CART_LINES_ADD_MUTATION,
3270
3986
  { cartId: existingCartId, lines }
3271
3987
  );
3272
3988
  const payload = res.cartLinesAdd;
@@ -3278,7 +3994,7 @@ var LimeBundleElement = class extends HTMLElement {
3278
3994
  }
3279
3995
  if (!checkoutUrl) {
3280
3996
  const res = await client.query(
3281
- import_core6.CART_CREATE_MUTATION,
3997
+ import_core7.CART_CREATE_MUTATION,
3282
3998
  { input: { lines } }
3283
3999
  );
3284
4000
  const payload = res.cartCreate;
@@ -3324,7 +4040,7 @@ var LimeBundleElement = class extends HTMLElement {
3324
4040
  const price = variant ? parseFloat(variant.price.amount) : 0;
3325
4041
  return sum + price * line.quantity;
3326
4042
  }, 0);
3327
- (0, import_core6.reportAddToCart)(
4043
+ (0, import_core7.reportAddToCart)(
3328
4044
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
3329
4045
  {
3330
4046
  bundleGid: bundle.id,
@@ -3344,7 +4060,8 @@ var LimeBundleElement = class extends HTMLElement {
3344
4060
  BUNDLE_BASE_CSS,
3345
4061
  BUNDLE_FIXED_CSS,
3346
4062
  BUNDLE_MIX_MATCH_CSS,
3347
- BUNDLE_VOLUME_CSS
4063
+ BUNDLE_VOLUME_CSS,
4064
+ BUNDLE_DROPDOWN_CSS
3348
4065
  ].join("\n");
3349
4066
  this.shadow.appendChild(style);
3350
4067
  if (this.shopCustomCss) {
@@ -3360,7 +4077,7 @@ var LimeBundleElement = class extends HTMLElement {
3360
4077
  container.setAttribute("aria-label", bundle.title);
3361
4078
  container.setAttribute("data-bundle-type", bundle.bundleType);
3362
4079
  container.setAttribute("data-bundle-gid", bundle.id);
3363
- (0, import_core7.applyWidgetConfigVars)(container, bundle.widgetConfig);
4080
+ (0, import_core8.applyWidgetConfigVars)(container, bundle.widgetConfig);
3364
4081
  this.renderCleanups.push(trackInputMode(container));
3365
4082
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
3366
4083
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
@@ -3396,8 +4113,8 @@ var LimeBundleElement = class extends HTMLElement {
3396
4113
  }
3397
4114
  setupImpressionFor(bundle, element) {
3398
4115
  if (!this.analyticsEnabled || !this.appUrl) return;
3399
- const cleanup = (0, import_core6.observeImpression)(element, () => {
3400
- (0, import_core6.reportImpression)(
4116
+ const cleanup = (0, import_core7.observeImpression)(element, () => {
4117
+ (0, import_core7.reportImpression)(
3401
4118
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
3402
4119
  {
3403
4120
  bundleGid: bundle.id,