@lime-bundles/widget 2.4.0 → 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
@@ -20,21 +20,399 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- LimeBundleElement: () => LimeBundleElement
23
+ LimeBundleElement: () => LimeBundleElement,
24
+ trackInputMode: () => trackInputMode
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
27
 
27
28
  // src/lime-bundle.ts
28
- var import_core5 = require("@lime-bundles/core");
29
+ var import_core7 = require("@lime-bundles/core");
29
30
 
30
31
  // src/renderers/fixed.ts
31
- var import_core4 = require("@lime-bundles/core");
32
+ var import_core5 = require("@lime-bundles/core");
32
33
 
33
- // src/renderers/pricing.ts
34
+ // src/dropdown/bind-dropdown.ts
34
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
+ }
35
410
 
36
- // src/renderers/countdown.ts
411
+ // src/renderers/pricing.ts
37
412
  var import_core2 = require("@lime-bundles/core");
413
+
414
+ // src/renderers/countdown.ts
415
+ var import_core3 = require("@lime-bundles/core");
38
416
  function renderCountdown(endsAtIso) {
39
417
  const parsed = parseIso(endsAtIso);
40
418
  if (parsed === null) return null;
@@ -61,7 +439,7 @@ function renderCountdown(endsAtIso) {
61
439
  stop();
62
440
  return;
63
441
  }
64
- timer.textContent = (0, import_core2.formatCountdown)(msLeft);
442
+ timer.textContent = (0, import_core3.formatCountdown)(msLeft);
65
443
  }
66
444
  function stop() {
67
445
  if (intervalId !== null) {
@@ -115,7 +493,7 @@ function el(tag, className, attrs = {}) {
115
493
  }
116
494
 
117
495
  // src/renderers/image.ts
118
- var import_core3 = require("@lime-bundles/core");
496
+ var import_core4 = require("@lime-bundles/core");
119
497
 
120
498
  // src/renderers/fixed.ts
121
499
  var PLACEHOLDER_THUMB_SVG = `
@@ -126,7 +504,7 @@ var PLACEHOLDER_THUMB_SVG = `
126
504
  </svg>`;
127
505
  function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
128
506
  const wc = bundle.widgetConfig;
129
- const qtyFor = (productId, variantId) => (0, import_core4.resolveBundleQty)(bundle, productId, variantId);
507
+ const qtyFor = (productId, variantId) => (0, import_core5.resolveBundleQty)(bundle, productId, variantId);
130
508
  const rows = [];
131
509
  let oosCount = 0;
132
510
  bundle.products.forEach((product, idx) => {
@@ -191,11 +569,13 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
191
569
  })
192
570
  );
193
571
  container.appendChild(root);
572
+ bindAllDropdowns(root);
573
+ onCleanup?.(() => unbindAllDropdowns(root));
194
574
  updatePricing();
195
575
  function updatePricing() {
196
576
  const totalCents = rows.reduce((sum, r) => {
197
577
  if (!r.selected) return sum;
198
- const unit = (0, import_core.parseCents)(r.selected.price.amount);
578
+ const unit = (0, import_core2.parseCents)(r.selected.price.amount);
199
579
  return sum + unit * r.qty;
200
580
  }, 0);
201
581
  const saleCents = computeSale(totalCents, bundle.discountConfig, rows);
@@ -211,13 +591,12 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
211
591
  }
212
592
  function buildRowState(bundle, product, productIndex) {
213
593
  const selectedVariantIds = bundle.selectedVariantIds?.[productIndex] ?? null;
214
- const available = product.variants.nodes.filter(
215
- (v) => v.availableForSale
216
- );
217
- const eligibleVariants = selectedVariantIds && selectedVariantIds.length > 0 ? available.filter((v) => selectedVariantIds.includes(v.id)) : available;
218
- const isOos = eligibleVariants.length === 0;
219
- const selected = eligibleVariants[0] ?? null;
220
- const qty = selected ? (0, import_core4.resolveBundleQty)(bundle, product.id, selected.id) : 1;
594
+ const merchantScoped = selectedVariantIds && selectedVariantIds.length > 0 ? product.variants.nodes.filter((v) => selectedVariantIds.includes(v.id)) : product.variants.nodes;
595
+ const firstInStock = merchantScoped.find((v) => v.availableForSale) ?? null;
596
+ const eligibleVariants = merchantScoped;
597
+ const isOos = !firstInStock;
598
+ const selected = firstInStock ?? merchantScoped[0] ?? null;
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,36 +743,97 @@ 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) {
368
- const select = document.createElement("select");
369
- select.className = "lb-bundle-variant-select";
370
- select.setAttribute("data-variant-select", "");
371
- select.name = `lb-variant-${state.product.id.replace(/^.*\//, "")}`;
372
- select.setAttribute(
373
- "aria-label",
374
- `Select variant for ${state.product.title}`
758
+ const optionNames = state.eligibleVariants[0].selectedOptions.map(
759
+ (o) => o.name
375
760
  );
376
- state.eligibleVariants.forEach((variant) => {
377
- const opt = document.createElement("option");
378
- opt.value = variant.id;
379
- opt.textContent = variant.title;
380
- if (variant.id === state.selected?.id) opt.selected = true;
381
- select.appendChild(opt);
382
- });
383
- select.addEventListener("change", () => {
384
- const variant = state.eligibleVariants.find(
385
- (v) => v.id === select.value
761
+ const productIdTail = state.product.id.replace(/^.*\//, "");
762
+ const optionSelects = [];
763
+ const resolveVariant = (values) => state.eligibleVariants.find(
764
+ (v) => v.selectedOptions.every((o, i) => o.value === values[i]) && v.selectedOptions.length === values.length
765
+ ) ?? null;
766
+ const syncSelectsToVariant = (variant) => {
767
+ variant.selectedOptions.forEach((o, i) => {
768
+ const sel = optionSelects[i];
769
+ if (sel && sel.value !== o.value) sel.value = o.value;
770
+ });
771
+ };
772
+ const isValueAvailable = (optionIndex, value, selected) => state.eligibleVariants.some((v) => {
773
+ if (!v.availableForSale) return false;
774
+ if (v.selectedOptions[optionIndex]?.value !== value) return false;
775
+ return v.selectedOptions.every(
776
+ (o, i) => i === optionIndex || o.value === selected[i]
386
777
  );
387
- if (!variant) return;
778
+ });
779
+ const recomputeDisabled = (selected) => {
780
+ optionSelects.forEach((sel, i) => {
781
+ Array.from(sel.options).forEach((opt) => {
782
+ opt.disabled = !isValueAvailable(i, opt.value, selected);
783
+ });
784
+ });
785
+ };
786
+ const handleChange = () => {
787
+ const values = optionSelects.map((s) => s.value);
788
+ const variant = resolveVariant(values);
789
+ if (!variant) {
790
+ if (state.selected) {
791
+ syncSelectsToVariant(state.selected);
792
+ recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
793
+ }
794
+ return;
795
+ }
388
796
  state.selected = variant;
389
797
  state.qty = qtyFor(state.product.id, variant.id);
390
798
  if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);
391
799
  applyVariantToRow(variant);
800
+ recomputeDisabled(variant.selectedOptions.map((o) => o.value));
392
801
  onVariantChange();
802
+ };
803
+ const groupsContainer = el("div", "lb-bundle-variant-option-groups");
804
+ optionNames.forEach((name2, position) => {
805
+ const group = el("div", "lb-bundle-variant-option-group");
806
+ const label = el("span", "lb-bundle-variant-option-label");
807
+ label.textContent = name2;
808
+ group.appendChild(label);
809
+ const select = document.createElement("select");
810
+ select.className = "lb-bundle-variant-select";
811
+ select.setAttribute("data-variant-option", "");
812
+ select.setAttribute("data-option-position", String(position + 1));
813
+ select.name = `lb-variant-${productIdTail}-${position + 1}`;
814
+ select.setAttribute("aria-label", name2);
815
+ const seen = /* @__PURE__ */ new Set();
816
+ state.eligibleVariants.forEach((v) => {
817
+ const value = v.selectedOptions[position]?.value;
818
+ if (!value || seen.has(value)) return;
819
+ seen.add(value);
820
+ const opt = document.createElement("option");
821
+ opt.value = value;
822
+ opt.textContent = value;
823
+ if (state.selected?.selectedOptions[position]?.value === value) {
824
+ opt.selected = true;
825
+ }
826
+ select.appendChild(opt);
827
+ });
828
+ select.addEventListener("change", handleChange);
829
+ optionSelects.push(select);
830
+ group.appendChild(select);
831
+ groupsContainer.appendChild(group);
393
832
  });
394
- info.appendChild(select);
833
+ info.appendChild(groupsContainer);
834
+ if (state.selected) {
835
+ recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
836
+ }
395
837
  } else if (state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1) {
396
838
  const badge = el("span", "lb-bundle-variant-badge");
397
839
  badge.textContent = state.eligibleVariants[0].title;
@@ -420,9 +862,9 @@ function renderPricingRow(bundle) {
420
862
  return {
421
863
  el: row,
422
864
  update({ totalCents, saleCents, savingsCents, currency }) {
423
- sale.textContent = (0, import_core.formatCents)(saleCents, currency);
865
+ sale.textContent = (0, import_core2.formatCents)(saleCents, currency);
424
866
  if (bundle.widgetConfig.pricing.showCompareAtPrice && savingsCents > 0) {
425
- compare.textContent = (0, import_core.formatCents)(totalCents, currency);
867
+ compare.textContent = (0, import_core2.formatCents)(totalCents, currency);
426
868
  compare.style.display = "";
427
869
  } else {
428
870
  compare.style.display = "none";
@@ -447,7 +889,7 @@ function renderSavingsBar() {
447
889
  return;
448
890
  }
449
891
  bar.style.display = "";
450
- amount.textContent = (0, import_core.formatCents)(savingsCents, currency);
892
+ amount.textContent = (0, import_core2.formatCents)(savingsCents, currency);
451
893
  }
452
894
  };
453
895
  }
@@ -469,7 +911,7 @@ function computeSale(totalCents, discount, rows) {
469
911
  let saleCents = 0;
470
912
  for (const r of rows) {
471
913
  if (!r.selected) continue;
472
- const unit = (0, import_core.parseCents)(r.selected.price.amount);
914
+ const unit = (0, import_core2.parseCents)(r.selected.price.amount);
473
915
  const off = Math.floor(unit * discount.discountValue / 100);
474
916
  const perUnit = Math.max(0, unit - off);
475
917
  saleCents += perUnit * r.qty;
@@ -480,6 +922,7 @@ function computeSale(totalCents, discount, rows) {
480
922
  }
481
923
 
482
924
  // src/renderers/mix-match.ts
925
+ var import_core6 = require("@lime-bundles/core");
483
926
  var PLACEHOLDER_THUMB_SVG2 = `
484
927
  <svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
485
928
  <rect x="4" y="4" width="20" height="20" rx="3"></rect>
@@ -547,9 +990,6 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
547
990
  showSearch: wc.showSearch,
548
991
  onAdd: (product, variant) => addSelection(product, variant),
549
992
  onRemove: (productId, variantId) => removeSelection(productId, variantId),
550
- countFor: (productId, variantId) => selections.filter(
551
- (s) => s.productId === productId && s.variantId === variantId
552
- ).length,
553
993
  isOverMax: () => selections.length >= maxQty
554
994
  });
555
995
  root.appendChild(modal.el);
@@ -559,7 +999,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
559
999
  if (cta.disabled) return;
560
1000
  const lines = selections.map((s) => ({
561
1001
  merchandiseId: s.variantId,
562
- quantity: 1,
1002
+ quantity: s.quantity,
563
1003
  attributes: [
564
1004
  { key: "_lime_bundle_gid", value: bundle.id },
565
1005
  { key: "_lime_bundle_type", value: bundle.bundleType }
@@ -578,6 +1018,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
578
1018
  })
579
1019
  );
580
1020
  container.appendChild(root);
1021
+ onCleanup?.(() => unbindAllDropdowns(root));
581
1022
  const firstEligible = eligible.find((ep) => !ep.isOos);
582
1023
  const firstVariant = firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];
583
1024
  if (firstEligible && firstVariant) {
@@ -586,14 +1027,15 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
586
1027
  productTitle: firstEligible.product.title,
587
1028
  variantId: firstVariant.id,
588
1029
  variantTitle: firstVariant.title,
589
- imageUrl: firstEligible.product.featuredImage?.url ?? null,
590
- priceCents: (0, import_core.parseCents)(firstVariant.price.amount),
591
- compareCents: firstVariant.compareAtPrice ? (0, import_core.parseCents)(firstVariant.compareAtPrice.amount) : null,
592
- 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)(
593
1034
  firstVariant.unitPrice,
594
1035
  firstVariant.unitPriceMeasurement,
595
1036
  currency
596
- )
1037
+ ),
1038
+ quantity: (0, import_core6.resolveBundleQty)(bundle, firstEligible.product.id, firstVariant.id)
597
1039
  });
598
1040
  }
599
1041
  afterMutation();
@@ -604,14 +1046,15 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
604
1046
  productTitle: product.title,
605
1047
  variantId: variant.id,
606
1048
  variantTitle: variant.title,
607
- imageUrl: product.featuredImage?.url ?? null,
608
- priceCents: (0, import_core.parseCents)(variant.price.amount),
609
- compareCents: variant.compareAtPrice ? (0, import_core.parseCents)(variant.compareAtPrice.amount) : null,
610
- 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)(
611
1053
  variant.unitPrice,
612
1054
  variant.unitPriceMeasurement,
613
1055
  currency
614
- )
1056
+ ),
1057
+ quantity: (0, import_core6.resolveBundleQty)(bundle, product.id, variant.id)
615
1058
  });
616
1059
  afterMutation();
617
1060
  }
@@ -678,7 +1121,7 @@ function renderHeader2(bundle) {
678
1121
  if (discountType === "percentage" && discountValue > 0) {
679
1122
  label = `-${Math.round(discountValue)}%`;
680
1123
  } else if (discountType === "fixed_amount" && discountValue > 0) {
681
- label = `-${(0, import_core.formatCents)(
1124
+ label = `-${(0, import_core2.formatCents)(
682
1125
  Math.round(discountValue * 100),
683
1126
  bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD"
684
1127
  )}`;
@@ -765,19 +1208,22 @@ function renderFilledSlot(selection, index, currency, onRemove) {
765
1208
  const thumb = el("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
766
1209
  if (selection.imageUrl) {
767
1210
  const img = document.createElement("img");
768
- img.src = (0, import_core3.transformImageUrl)(selection.imageUrl, {
769
- width: import_core3.THUMB_PX,
770
- 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,
771
1214
  crop: "center"
772
1215
  });
773
1216
  img.alt = selection.productTitle;
774
- img.width = import_core3.THUMB_PX;
775
- img.height = import_core3.THUMB_PX;
1217
+ img.width = import_core4.THUMB_PX;
1218
+ img.height = import_core4.THUMB_PX;
776
1219
  img.loading = "lazy";
777
1220
  thumb.appendChild(img);
778
1221
  } else {
779
1222
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
780
1223
  }
1224
+ const qtyBadge = el("span", "lb-bundle-qty-badge");
1225
+ qtyBadge.textContent = String(selection.quantity);
1226
+ thumb.appendChild(qtyBadge);
781
1227
  slot.appendChild(thumb);
782
1228
  const info = el("div", "lb-mix-match__filled-info");
783
1229
  const title = el("span", "lb-mix-match__filled-title");
@@ -788,14 +1234,16 @@ function renderFilledSlot(selection, index, currency, onRemove) {
788
1234
  variant.textContent = selection.variantTitle;
789
1235
  info.appendChild(variant);
790
1236
  }
1237
+ const linePrice = selection.priceCents * selection.quantity;
1238
+ const lineCompare = selection.compareCents !== null ? selection.compareCents * selection.quantity : null;
791
1239
  const priceWrap = el("span", "lb-mix-match__filled-price");
792
- if (selection.compareCents && selection.compareCents > selection.priceCents) {
1240
+ if (lineCompare !== null && lineCompare > linePrice) {
793
1241
  const compare = el("span", "lb-mix-match__filled-compare");
794
- compare.textContent = (0, import_core.formatCents)(selection.compareCents, currency);
1242
+ compare.textContent = (0, import_core2.formatCents)(lineCompare, currency);
795
1243
  priceWrap.appendChild(compare);
796
1244
  }
797
1245
  const priceEl = document.createElement("span");
798
- priceEl.textContent = (0, import_core.formatCents)(selection.priceCents, currency);
1246
+ priceEl.textContent = (0, import_core2.formatCents)(linePrice, currency);
799
1247
  priceWrap.appendChild(priceEl);
800
1248
  info.appendChild(priceWrap);
801
1249
  if (selection.unitPriceLabel) {
@@ -836,15 +1284,18 @@ function renderPricingSection(showCompareAtPrice) {
836
1284
  return;
837
1285
  }
838
1286
  wrap.style.display = "";
839
- const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
840
- const saleCents = (0, import_core.computeBundleSaleCents)(totalCents, bundle.discountConfig);
1287
+ const totalCents = selections.reduce(
1288
+ (s, sel) => s + sel.priceCents * sel.quantity,
1289
+ 0
1290
+ );
1291
+ const saleCents = (0, import_core2.computeBundleSaleCents)(totalCents, bundle.discountConfig);
841
1292
  if (showCompareAtPrice && totalCents > saleCents) {
842
- compare.textContent = (0, import_core.formatCents)(totalCents, currency);
1293
+ compare.textContent = (0, import_core2.formatCents)(totalCents, currency);
843
1294
  compare.style.display = "";
844
1295
  } else {
845
1296
  compare.style.display = "none";
846
1297
  }
847
- sale.textContent = (0, import_core.formatCents)(saleCents, currency);
1298
+ sale.textContent = (0, import_core2.formatCents)(saleCents, currency);
848
1299
  }
849
1300
  return { el: wrap, update };
850
1301
  }
@@ -863,15 +1314,18 @@ function renderSavingsBar2() {
863
1314
  wrap.style.display = "none";
864
1315
  return;
865
1316
  }
866
- const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
867
- const saleCents = (0, import_core.computeBundleSaleCents)(totalCents, bundle.discountConfig);
1317
+ const totalCents = selections.reduce(
1318
+ (s, sel) => s + sel.priceCents * sel.quantity,
1319
+ 0
1320
+ );
1321
+ const saleCents = (0, import_core2.computeBundleSaleCents)(totalCents, bundle.discountConfig);
868
1322
  const savings = Math.max(0, totalCents - saleCents);
869
1323
  if (savings <= 0) {
870
1324
  wrap.style.display = "none";
871
1325
  return;
872
1326
  }
873
1327
  wrap.style.display = "";
874
- amount.textContent = (0, import_core.formatCents)(savings, currency);
1328
+ amount.textContent = (0, import_core2.formatCents)(savings, currency);
875
1329
  }
876
1330
  return { el: wrap, update };
877
1331
  }
@@ -957,8 +1411,8 @@ function renderModal(bundle, eligible, currency, handlers) {
957
1411
  rowsBuilt = true;
958
1412
  list.innerHTML = "";
959
1413
  eligible.forEach((ep) => {
960
- const availableVariants = ep.variants.filter((v) => v.availableForSale);
961
- const firstAvailVariant = availableVariants[0] ?? ep.firstAvailableVariant ?? ep.variants[0];
1414
+ const availableVariants = ep.variants;
1415
+ const firstAvailVariant = ep.variants.find((v) => v.availableForSale) ?? ep.firstAvailableVariant ?? ep.variants[0];
962
1416
  if (!firstAvailVariant) return;
963
1417
  let currentVariant = firstAvailVariant;
964
1418
  const productEl = el(
@@ -967,23 +1421,28 @@ function renderModal(bundle, eligible, currency, handlers) {
967
1421
  { "data-product-id": ep.product.id.replace(/^.*\//, "") }
968
1422
  );
969
1423
  const thumb = el("div", "lb-mix-match__modal-product-thumb");
970
- if (ep.product.featuredImage) {
971
- const img = document.createElement("img");
972
- img.src = (0, import_core3.transformImageUrl)(ep.product.featuredImage.url, {
973
- width: import_core3.THUMB_PX,
974
- 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,
975
1432
  crop: "center"
976
1433
  });
977
- img.alt = ep.product.featuredImage.altText ?? ep.product.title;
978
- img.width = import_core3.THUMB_PX;
979
- img.height = import_core3.THUMB_PX;
980
- img.loading = "lazy";
981
- 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);
982
1439
  } else {
983
1440
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
984
1441
  }
985
1442
  const countBadge = el("span", "lb-bundle-qty-badge");
986
- countBadge.style.display = "none";
1443
+ countBadge.textContent = String(
1444
+ (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1445
+ );
987
1446
  thumb.appendChild(countBadge);
988
1447
  productEl.appendChild(thumb);
989
1448
  const info = el("div", "lb-mix-match__modal-product-info");
@@ -991,8 +1450,8 @@ function renderModal(bundle, eligible, currency, handlers) {
991
1450
  title.textContent = ep.product.title;
992
1451
  info.appendChild(title);
993
1452
  const price = el("p", "lb-mix-match__modal-product-price");
994
- price.textContent = (0, import_core.formatCents)(
995
- (0, import_core.parseCents)(currentVariant.price.amount),
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),
996
1455
  currency
997
1456
  );
998
1457
  info.appendChild(price);
@@ -1000,7 +1459,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1000
1459
  "p",
1001
1460
  "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price"
1002
1461
  );
1003
- const initialUnitText = (0, import_core.formatUnitPrice)(
1462
+ const initialUnitText = (0, import_core2.formatUnitPrice)(
1004
1463
  currentVariant.unitPrice,
1005
1464
  currentVariant.unitPriceMeasurement,
1006
1465
  currency
@@ -1019,31 +1478,56 @@ function renderModal(bundle, eligible, currency, handlers) {
1019
1478
  }
1020
1479
  };
1021
1480
  if (availableVariants.length > 1) {
1022
- const select = document.createElement("select");
1023
- select.className = "lb-mix-match__variant-select";
1024
- select.setAttribute("data-variant-select", "");
1025
- select.name = `lb-variant-${ep.product.id.replace(/^.*\//, "")}`;
1026
- select.setAttribute(
1027
- "aria-label",
1028
- `Select variant for ${ep.product.title}`
1481
+ const optionNames = availableVariants[0].selectedOptions.map(
1482
+ (o) => o.name
1029
1483
  );
1030
- availableVariants.forEach((v) => {
1031
- const opt = document.createElement("option");
1032
- opt.value = v.id;
1033
- opt.textContent = v.title;
1034
- if (v.id === firstAvailVariant.id) opt.selected = true;
1035
- select.appendChild(opt);
1484
+ const productIdTail = ep.product.id.replace(/^.*\//, "");
1485
+ const optionSelects = [];
1486
+ const resolveVariant = (values) => availableVariants.find(
1487
+ (v) => v.selectedOptions.length === values.length && v.selectedOptions.every((o, i) => o.value === values[i])
1488
+ ) ?? null;
1489
+ const syncSelectsToVariant = (v) => {
1490
+ v.selectedOptions.forEach((o, i) => {
1491
+ const sel = optionSelects[i];
1492
+ if (sel && sel.value !== o.value) sel.value = o.value;
1493
+ });
1494
+ };
1495
+ const isValueAvailable = (optionIndex, value, selected) => availableVariants.some((v) => {
1496
+ if (!v.availableForSale) return false;
1497
+ if (v.selectedOptions[optionIndex]?.value !== value) return false;
1498
+ return v.selectedOptions.every(
1499
+ (o, i) => i === optionIndex || o.value === selected[i]
1500
+ );
1036
1501
  });
1037
- select.addEventListener("change", () => {
1038
- const next = availableVariants.find((v) => v.id === select.value);
1039
- if (!next) return;
1502
+ const recomputeDisabled = (selected) => {
1503
+ optionSelects.forEach((sel, i) => {
1504
+ Array.from(sel.options).forEach((opt) => {
1505
+ opt.disabled = !isValueAvailable(i, opt.value, selected);
1506
+ });
1507
+ });
1508
+ };
1509
+ const handleChange = () => {
1510
+ const values = optionSelects.map((s) => s.value);
1511
+ const next = resolveVariant(values);
1512
+ if (!next) {
1513
+ syncSelectsToVariant(currentVariant);
1514
+ recomputeDisabled(
1515
+ currentVariant.selectedOptions.map((o) => o.value)
1516
+ );
1517
+ return;
1518
+ }
1040
1519
  currentVariant = next;
1041
1520
  row.variant = next;
1042
- price.textContent = (0, import_core.formatCents)(
1043
- (0, import_core.parseCents)(currentVariant.price.amount),
1521
+ const nextQty = (0, import_core6.resolveBundleQty)(
1522
+ bundle,
1523
+ ep.product.id,
1524
+ currentVariant.id
1525
+ );
1526
+ price.textContent = (0, import_core2.formatCents)(
1527
+ (0, import_core2.parseCents)(currentVariant.price.amount) * nextQty,
1044
1528
  currency
1045
1529
  );
1046
- const nextUnitText = (0, import_core.formatUnitPrice)(
1530
+ const nextUnitText = (0, import_core2.formatUnitPrice)(
1047
1531
  currentVariant.unitPrice,
1048
1532
  currentVariant.unitPriceMeasurement,
1049
1533
  currency
@@ -1055,9 +1539,52 @@ function renderModal(bundle, eligible, currency, handlers) {
1055
1539
  unitPrice.textContent = "";
1056
1540
  unitPrice.hidden = true;
1057
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
+ }
1551
+ recomputeDisabled(next.selectedOptions.map((o) => o.value));
1058
1552
  rowUpdateCount();
1553
+ };
1554
+ const groupsContainer = el("div", "lb-bundle-variant-option-groups");
1555
+ optionNames.forEach((name, position) => {
1556
+ const group = el("div", "lb-bundle-variant-option-group");
1557
+ const label = el("span", "lb-bundle-variant-option-label");
1558
+ label.textContent = name;
1559
+ group.appendChild(label);
1560
+ const select = document.createElement("select");
1561
+ select.className = "lb-mix-match__variant-select";
1562
+ select.setAttribute("data-variant-option", "");
1563
+ select.setAttribute("data-option-position", String(position + 1));
1564
+ select.name = `lb-variant-${productIdTail}-${position + 1}`;
1565
+ select.setAttribute("aria-label", name);
1566
+ const seen = /* @__PURE__ */ new Set();
1567
+ availableVariants.forEach((v) => {
1568
+ const value = v.selectedOptions[position]?.value;
1569
+ if (!value || seen.has(value)) return;
1570
+ seen.add(value);
1571
+ const opt = document.createElement("option");
1572
+ opt.value = value;
1573
+ opt.textContent = value;
1574
+ if (firstAvailVariant.selectedOptions[position]?.value === value) {
1575
+ opt.selected = true;
1576
+ }
1577
+ select.appendChild(opt);
1578
+ });
1579
+ select.addEventListener("change", handleChange);
1580
+ optionSelects.push(select);
1581
+ group.appendChild(select);
1582
+ groupsContainer.appendChild(group);
1059
1583
  });
1060
- info.appendChild(select);
1584
+ info.appendChild(groupsContainer);
1585
+ recomputeDisabled(
1586
+ firstAvailVariant.selectedOptions.map((o) => o.value)
1587
+ );
1061
1588
  } else if (availableVariants.length === 1 && firstAvailVariant.title !== "Default Title") {
1062
1589
  const variantLabel = el("span", "lb-mix-match__filled-variant");
1063
1590
  variantLabel.textContent = firstAvailVariant.title;
@@ -1070,13 +1597,9 @@ function renderModal(bundle, eligible, currency, handlers) {
1070
1597
  }
1071
1598
  productEl.appendChild(info);
1072
1599
  const rowUpdateCount = () => {
1073
- const count = handlers.countFor(ep.product.id, currentVariant.id);
1074
- if (count > 0) {
1075
- countBadge.textContent = String(count);
1076
- countBadge.style.display = "";
1077
- } else {
1078
- countBadge.style.display = "none";
1079
- }
1600
+ countBadge.textContent = String(
1601
+ (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
1602
+ );
1080
1603
  };
1081
1604
  if (!ep.isOos) {
1082
1605
  const addBtn = document.createElement("button");
@@ -1086,6 +1609,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1086
1609
  addBtn.addEventListener("click", () => {
1087
1610
  if (handlers.isOverMax()) return;
1088
1611
  handlers.onAdd(ep.product, currentVariant);
1612
+ close();
1089
1613
  });
1090
1614
  productEl.appendChild(addBtn);
1091
1615
  }
@@ -1094,6 +1618,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1094
1618
  productRows.push(row);
1095
1619
  list.appendChild(productEl);
1096
1620
  });
1621
+ bindAllDropdowns(list);
1097
1622
  refreshCounts();
1098
1623
  }
1099
1624
  function applySearch() {
@@ -1197,7 +1722,7 @@ function renderVolumeBundle(container, bundle, onAddToCart, onCleanup) {
1197
1722
  const product = bundle.products[0];
1198
1723
  const variant = product?.variants.nodes.find((v) => v.availableForSale);
1199
1724
  if (!variant && wc.outOfStockBehavior === "hide") return;
1200
- const basePriceCents = variant ? (0, import_core.parseCents)(variant.price.amount) : 0;
1725
+ const basePriceCents = variant ? (0, import_core2.parseCents)(variant.price.amount) : 0;
1201
1726
  const currency = variant?.price.currencyCode ?? "USD";
1202
1727
  const discountType = bundle.discountConfig.discountType;
1203
1728
  const resolved = bundle.volumeTiers.map((tier, index) => {
@@ -1376,13 +1901,13 @@ function renderTierCard(r, isSelected, currency, popularLabel, showComparePrice,
1376
1901
  const price = el("span", "lb-volume__tier-price");
1377
1902
  if (showComparePrice && r.pricePerUnitCents < r.basePricePerUnitCents) {
1378
1903
  const compare = el("span", "lb-volume__tier-compare");
1379
- compare.textContent = (0, import_core.formatCents)(r.basePricePerUnitCents, currency);
1904
+ compare.textContent = (0, import_core2.formatCents)(r.basePricePerUnitCents, currency);
1380
1905
  price.appendChild(compare);
1381
1906
  }
1382
1907
  if (showPerUnitPrice) {
1383
1908
  const each = document.createElement("span");
1384
1909
  each.setAttribute("data-tier-price-each", "");
1385
- each.textContent = (0, import_core.formatCents)(r.pricePerUnitCents, currency);
1910
+ each.textContent = (0, import_core2.formatCents)(r.pricePerUnitCents, currency);
1386
1911
  price.appendChild(each);
1387
1912
  const unit = el("span", "lb-volume__tier-unit");
1388
1913
  unit.textContent = " each";
@@ -1421,11 +1946,11 @@ function renderPricingRow2(resolved, selectedIndex, currency, showItemCount, sho
1421
1946
  const compare = el("span", "lb-bundle-compare-price", {
1422
1947
  "data-compare-price": ""
1423
1948
  });
1424
- compare.textContent = (0, import_core.formatCents)(undiscountedCents, currency);
1949
+ compare.textContent = (0, import_core2.formatCents)(undiscountedCents, currency);
1425
1950
  prices.appendChild(compare);
1426
1951
  }
1427
1952
  const sale = el("span", "lb-bundle-sale-price", { "data-total-price": "" });
1428
- sale.textContent = (0, import_core.formatCents)(totalCents, currency);
1953
+ sale.textContent = (0, import_core2.formatCents)(totalCents, currency);
1429
1954
  prices.appendChild(sale);
1430
1955
  row.appendChild(prices);
1431
1956
  return row;
@@ -1441,7 +1966,7 @@ function renderSavingsBar3(resolved, selectedIndex, currency) {
1441
1966
  labelEl.textContent = "You save";
1442
1967
  bar.appendChild(labelEl);
1443
1968
  const amount = el("span", "", { "data-savings-amount": "" });
1444
- amount.textContent = (0, import_core.formatCents)(savings, currency);
1969
+ amount.textContent = (0, import_core2.formatCents)(savings, currency);
1445
1970
  bar.appendChild(amount);
1446
1971
  return bar;
1447
1972
  }
@@ -1462,7 +1987,7 @@ function badgeFor(resolved, currency, discountType) {
1462
1987
  const { tier } = resolved;
1463
1988
  if (discountType === "fixed_amount") {
1464
1989
  const amount = tier.amount ?? 0;
1465
- 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)}`;
1466
1991
  return null;
1467
1992
  }
1468
1993
  if (discountType === "percentage") {
@@ -1489,7 +2014,62 @@ function clamp(n, min, max) {
1489
2014
  }
1490
2015
 
1491
2016
  // src/lime-bundle.ts
1492
- var import_core6 = require("@lime-bundles/core");
2017
+ var import_core8 = require("@lime-bundles/core");
2018
+
2019
+ // src/utils/input-mode.ts
2020
+ var NAV_KEYS = /* @__PURE__ */ new Set([
2021
+ "Tab",
2022
+ "ArrowUp",
2023
+ "ArrowDown",
2024
+ "ArrowLeft",
2025
+ "ArrowRight",
2026
+ "Home",
2027
+ "End",
2028
+ "PageUp",
2029
+ "PageDown",
2030
+ "Enter",
2031
+ " ",
2032
+ "Escape"
2033
+ ]);
2034
+ var targets = /* @__PURE__ */ new Set();
2035
+ var listenersAttached = false;
2036
+ function setAll(on) {
2037
+ const off = on === "using-mouse" ? "using-keyboard" : "using-mouse";
2038
+ for (const el2 of targets) {
2039
+ el2.classList.add(on);
2040
+ el2.classList.remove(off);
2041
+ }
2042
+ }
2043
+ function onKeyDown(e) {
2044
+ if (NAV_KEYS.has(e.key)) setAll("using-keyboard");
2045
+ }
2046
+ function onPointerDown() {
2047
+ setAll("using-mouse");
2048
+ }
2049
+ function attachListeners() {
2050
+ if (listenersAttached) return;
2051
+ listenersAttached = true;
2052
+ document.addEventListener("keydown", onKeyDown, true);
2053
+ document.addEventListener("pointerdown", onPointerDown, true);
2054
+ }
2055
+ function detachListeners() {
2056
+ if (!listenersAttached) return;
2057
+ listenersAttached = false;
2058
+ document.removeEventListener("keydown", onKeyDown, true);
2059
+ document.removeEventListener("pointerdown", onPointerDown, true);
2060
+ }
2061
+ function trackInputMode(target) {
2062
+ target.classList.add("using-mouse");
2063
+ target.classList.remove("using-keyboard");
2064
+ targets.add(target);
2065
+ attachListeners();
2066
+ return () => {
2067
+ targets.delete(target);
2068
+ target.classList.remove("using-mouse");
2069
+ target.classList.remove("using-keyboard");
2070
+ if (targets.size === 0) detachListeners();
2071
+ };
2072
+ }
1493
2073
 
1494
2074
  // src/styles/bundle-css.ts
1495
2075
  var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle widget types */
@@ -1504,6 +2084,11 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1504
2084
  --lb-thumbnail-bg: #F0F0F0;
1505
2085
  --lb-widget-pad: 20px;
1506
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;
1507
2092
 
1508
2093
  font-family: inherit;
1509
2094
  font-size: 16px;
@@ -1775,6 +2360,56 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1775
2360
  margin-top: 8px;
1776
2361
  }
1777
2362
 
2363
+ /* Per-option variant pickers. Each option's label + select sit inside a
2364
+ .lb-bundle-variant-option-group (flex column, 2px gap between label and
2365
+ select); the groups stack inside a .lb-bundle-variant-option-groups
2366
+ parent (flex column, 12px gap between groups). The parent owns the top
2367
+ offset from the preceding unit-price line, so individual labels and
2368
+ selects don't carry their own vertical margins. */
2369
+ .lb-bundle-variant-option-groups {
2370
+ display: flex;
2371
+ flex-direction: column;
2372
+ gap: 12px;
2373
+ margin-top: 8px;
2374
+ }
2375
+
2376
+ .lb-bundle-variant-option-group {
2377
+ display: flex;
2378
+ flex-direction: column;
2379
+ gap: 2px;
2380
+ }
2381
+
2382
+ .lb-bundle-variant-option-label {
2383
+ display: block;
2384
+ margin: 0;
2385
+ font-size: 12px;
2386
+ line-height: 16px;
2387
+ font-weight: 600;
2388
+ letter-spacing: 0.05em;
2389
+ text-transform: uppercase;
2390
+ color: color-mix(in srgb, var(--lb-text) 60%, transparent);
2391
+ }
2392
+
2393
+ .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {
2394
+ margin-top: 4px;
2395
+ }
2396
+
2397
+ /* Focus-ring suppression for mouse users. A small JS helper \u2014 see
2398
+ packages/widget/src/utils/input-mode.ts and bundle-widget.js \u2014 toggles
2399
+ .using-mouse / .using-keyboard on the widget root (or html in the Liquid
2400
+ path) based on the customer's current input device. Default is mouse, so
2401
+ click-to-focus doesn't leave a keyboard-style ring. The modal overlay
2402
+ gets its own selector because the Liquid path reparents it to body,
2403
+ outside the widget root. */
2404
+ .using-mouse .lb-bundle-widget :focus,
2405
+ .using-mouse .lb-bundle-widget :focus-visible,
2406
+ .using-mouse .lb-mix-match__modal-overlay :focus,
2407
+ .using-mouse .lb-mix-match__modal-overlay :focus-visible {
2408
+ outline: none;
2409
+ outline-offset: 0;
2410
+ box-shadow: none;
2411
+ }
2412
+
1778
2413
  .lb-bundle-quantity {
1779
2414
  font-size: 12px;
1780
2415
  line-height: 16px;
@@ -2008,6 +2643,22 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2008
2643
  flex-direction: column;
2009
2644
  gap: 0;
2010
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;
2011
2662
  }
2012
2663
 
2013
2664
  /* Fixed bundles: product rows */
@@ -2032,9 +2683,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2032
2683
  border: none;
2033
2684
  }
2034
2685
 
2035
- /* Variant picker select \u2014 styled to match the variant badge aesthetic */
2686
+ /* Variant picker select \u2014 styled to match the variant badge aesthetic.
2687
+ Sits inside .lb-bundle-variant-option-group so vertical spacing is owned
2688
+ by the group/groups flex gap, not the select itself. */
2036
2689
  .lb-bundle-variant-select {
2037
- margin-top: 8px;
2038
2690
  display: inline-block;
2039
2691
  border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
2040
2692
  border-radius: var(--lb-variant-radius);
@@ -2051,7 +2703,8 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2051
2703
  background-repeat: no-repeat;
2052
2704
  background-position: right 8px center;
2053
2705
  background-size: 12px;
2054
- max-width: 100%;
2706
+ width: 50%;
2707
+ max-width: 50%;
2055
2708
  }
2056
2709
 
2057
2710
  .lb-bundle-variant-select:focus-visible {
@@ -2061,6 +2714,28 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2061
2714
  `;
2062
2715
  var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2063
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
+
2064
2739
  /* === Progress Bar === */
2065
2740
  .lb-mix-match__progress {
2066
2741
  margin-bottom: 16px;
@@ -2458,7 +3133,6 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2458
3133
 
2459
3134
  .lb-mix-match__variant-select {
2460
3135
  font-size: 12px;
2461
- margin: 4px 0 0;
2462
3136
  padding: 4px 24px 4px 8px;
2463
3137
  border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
2464
3138
  border-radius: var(--lb-picker-variant-radius);
@@ -2471,7 +3145,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2471
3145
  font-family: inherit;
2472
3146
  min-height: 32px;
2473
3147
  cursor: pointer;
2474
- max-width: 120px;
3148
+ width: 50%;
3149
+ max-width: 50%;
2475
3150
  appearance: none;
2476
3151
  -webkit-appearance: none;
2477
3152
  }
@@ -2555,6 +3230,11 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2555
3230
  .lb-mix-match__modal-overlay--open .lb-mix-match__modal {
2556
3231
  transform: translateY(0);
2557
3232
  }
3233
+
3234
+ .lb-mix-match__variant-select {
3235
+ width: 80%;
3236
+ max-width: 80%;
3237
+ }
2558
3238
  }
2559
3239
 
2560
3240
  /* === Reduced Motion === */
@@ -2572,6 +3252,22 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2572
3252
  display: flex;
2573
3253
  flex-direction: column;
2574
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;
2575
3271
  }
2576
3272
 
2577
3273
  .lb-volume__tier {
@@ -2676,6 +3372,259 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2676
3372
  }
2677
3373
 
2678
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
+ }
2679
3628
  `;
2680
3629
  var BUNDLE_SKELETON_CSS = `/* Lime Bundles \u2014 web-component loading skeleton (not mirrored to theme assets) */
2681
3630
 
@@ -2874,7 +3823,7 @@ var LimeBundleElement = class extends HTMLElement {
2874
3823
  const controller = new AbortController();
2875
3824
  this.abortController = controller;
2876
3825
  this.renderLoading();
2877
- const client = (0, import_core5.createStorefrontClient)({
3826
+ const client = (0, import_core7.createStorefrontClient)({
2878
3827
  shopDomain: this.shopDomain,
2879
3828
  accessToken: this.storefrontToken
2880
3829
  });
@@ -2899,7 +3848,7 @@ var LimeBundleElement = class extends HTMLElement {
2899
3848
  handle
2900
3849
  );
2901
3850
  }
2902
- const cssPromise = client.query(import_core5.SHOP_CUSTOM_CSS_QUERY, void 0, {
3851
+ const cssPromise = client.query(import_core7.SHOP_CUSTOM_CSS_QUERY, void 0, {
2903
3852
  signal: controller.signal
2904
3853
  }).catch(() => null);
2905
3854
  await bundlePromise;
@@ -2911,8 +3860,8 @@ var LimeBundleElement = class extends HTMLElement {
2911
3860
  }
2912
3861
  const css = await cssPromise;
2913
3862
  if (css?.shop?.metafield?.value) {
2914
- (0, import_core5.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
2915
- const sanitized = (0, import_core5.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);
2916
3865
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
2917
3866
  }
2918
3867
  await this.applyABVariants();
@@ -2940,14 +3889,14 @@ var LimeBundleElement = class extends HTMLElement {
2940
3889
  this.bundles.map(async (bundle) => {
2941
3890
  if (!bundle.abTestId || !bundle.abVariantB) return bundle;
2942
3891
  try {
2943
- const assignment = await (0, import_core5.getABTestAssignment)(
3892
+ const assignment = await (0, import_core7.getABTestAssignment)(
2944
3893
  this.appUrl,
2945
3894
  this.shopDomain,
2946
3895
  bundle.abTestId,
2947
3896
  bundle.id
2948
3897
  );
2949
3898
  if (assignment?.variant === "B") {
2950
- return (0, import_core5.applyABVariantB)(bundle);
3899
+ return (0, import_core7.applyABVariantB)(bundle);
2951
3900
  }
2952
3901
  } catch (err) {
2953
3902
  console.warn(
@@ -2962,7 +3911,7 @@ var LimeBundleElement = class extends HTMLElement {
2962
3911
  }
2963
3912
  async fetchSingleBundle(client, signal) {
2964
3913
  const data = await client.query(
2965
- import_core5.BUNDLE_METAOBJECT_QUERY,
3914
+ import_core7.BUNDLE_METAOBJECT_QUERY,
2966
3915
  { id: this.bundleGid },
2967
3916
  { signal }
2968
3917
  );
@@ -2970,7 +3919,7 @@ var LimeBundleElement = class extends HTMLElement {
2970
3919
  this.bundles = [];
2971
3920
  return;
2972
3921
  }
2973
- const parsed = (0, import_core5.parseMetaobjectBundle)(
3922
+ const parsed = (0, import_core7.parseMetaobjectBundle)(
2974
3923
  data.metaobject.id,
2975
3924
  data.metaobject.fields
2976
3925
  );
@@ -2978,7 +3927,7 @@ var LimeBundleElement = class extends HTMLElement {
2978
3927
  }
2979
3928
  async fetchProductBundles(client, signal, productHandle) {
2980
3929
  const data = await client.query(
2981
- import_core5.BUNDLES_FOR_PRODUCT_QUERY,
3930
+ import_core7.BUNDLES_FOR_PRODUCT_QUERY,
2982
3931
  { handle: productHandle },
2983
3932
  { signal }
2984
3933
  );
@@ -2989,7 +3938,7 @@ var LimeBundleElement = class extends HTMLElement {
2989
3938
  const refs = data.product.metafield?.references?.nodes ?? [];
2990
3939
  const bundles = [];
2991
3940
  for (const ref of refs) {
2992
- const parsed = (0, import_core5.parseMetaobjectBundle)(ref.id, ref.fields);
3941
+ const parsed = (0, import_core7.parseMetaobjectBundle)(ref.id, ref.fields);
2993
3942
  if (parsed) bundles.push(parsed);
2994
3943
  }
2995
3944
  this.bundles = bundles;
@@ -3022,7 +3971,7 @@ var LimeBundleElement = class extends HTMLElement {
3022
3971
  */
3023
3972
  async defaultAddToCart(lines) {
3024
3973
  if (typeof window === "undefined") return;
3025
- const client = (0, import_core5.createStorefrontClient)({
3974
+ const client = (0, import_core7.createStorefrontClient)({
3026
3975
  shopDomain: this.shopDomain,
3027
3976
  accessToken: this.storefrontToken
3028
3977
  });
@@ -3033,7 +3982,7 @@ var LimeBundleElement = class extends HTMLElement {
3033
3982
  let checkoutUrl = null;
3034
3983
  if (existingCartId) {
3035
3984
  const res = await client.query(
3036
- import_core5.CART_LINES_ADD_MUTATION,
3985
+ import_core7.CART_LINES_ADD_MUTATION,
3037
3986
  { cartId: existingCartId, lines }
3038
3987
  );
3039
3988
  const payload = res.cartLinesAdd;
@@ -3045,7 +3994,7 @@ var LimeBundleElement = class extends HTMLElement {
3045
3994
  }
3046
3995
  if (!checkoutUrl) {
3047
3996
  const res = await client.query(
3048
- import_core5.CART_CREATE_MUTATION,
3997
+ import_core7.CART_CREATE_MUTATION,
3049
3998
  { input: { lines } }
3050
3999
  );
3051
4000
  const payload = res.cartCreate;
@@ -3091,7 +4040,7 @@ var LimeBundleElement = class extends HTMLElement {
3091
4040
  const price = variant ? parseFloat(variant.price.amount) : 0;
3092
4041
  return sum + price * line.quantity;
3093
4042
  }, 0);
3094
- (0, import_core5.reportAddToCart)(
4043
+ (0, import_core7.reportAddToCart)(
3095
4044
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
3096
4045
  {
3097
4046
  bundleGid: bundle.id,
@@ -3111,7 +4060,8 @@ var LimeBundleElement = class extends HTMLElement {
3111
4060
  BUNDLE_BASE_CSS,
3112
4061
  BUNDLE_FIXED_CSS,
3113
4062
  BUNDLE_MIX_MATCH_CSS,
3114
- BUNDLE_VOLUME_CSS
4063
+ BUNDLE_VOLUME_CSS,
4064
+ BUNDLE_DROPDOWN_CSS
3115
4065
  ].join("\n");
3116
4066
  this.shadow.appendChild(style);
3117
4067
  if (this.shopCustomCss) {
@@ -3127,7 +4077,8 @@ var LimeBundleElement = class extends HTMLElement {
3127
4077
  container.setAttribute("aria-label", bundle.title);
3128
4078
  container.setAttribute("data-bundle-type", bundle.bundleType);
3129
4079
  container.setAttribute("data-bundle-gid", bundle.id);
3130
- (0, import_core6.applyWidgetConfigVars)(container, bundle.widgetConfig);
4080
+ (0, import_core8.applyWidgetConfigVars)(container, bundle.widgetConfig);
4081
+ this.renderCleanups.push(trackInputMode(container));
3131
4082
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
3132
4083
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
3133
4084
  switch (bundle.bundleType) {
@@ -3162,8 +4113,8 @@ var LimeBundleElement = class extends HTMLElement {
3162
4113
  }
3163
4114
  setupImpressionFor(bundle, element) {
3164
4115
  if (!this.analyticsEnabled || !this.appUrl) return;
3165
- const cleanup = (0, import_core5.observeImpression)(element, () => {
3166
- (0, import_core5.reportImpression)(
4116
+ const cleanup = (0, import_core7.observeImpression)(element, () => {
4117
+ (0, import_core7.reportImpression)(
3167
4118
  { shopDomain: this.shopDomain, appUrl: this.appUrl },
3168
4119
  {
3169
4120
  bundleGid: bundle.id,
@@ -3230,6 +4181,7 @@ if (typeof customElements !== "undefined" && !customElements.get("lime-bundle"))
3230
4181
  }
3231
4182
  // Annotate the CommonJS export names for ESM import in node:
3232
4183
  0 && (module.exports = {
3233
- LimeBundleElement
4184
+ LimeBundleElement,
4185
+ trackInputMode
3234
4186
  });
3235
4187
  //# sourceMappingURL=index.cjs.map