@lime-bundles/widget 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -62,4 +62,25 @@ declare class LimeBundleElement extends HTMLElement {
62
62
  private renderError;
63
63
  }
64
64
 
65
- export { LimeBundleElement };
65
+ /**
66
+ * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a
67
+ * target element so CSS can scope focus styles by the customer's current
68
+ * input device. Default is mouse; the first keyboard-navigation keypress
69
+ * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips
70
+ * to keyboard mode, and the next pointer click flips back.
71
+ *
72
+ * Multiple targets share one pair of document-level listeners — installed
73
+ * on the first `trackInputMode` call, removed when the last target is
74
+ * released. Safe to call across every widget instance on a page without
75
+ * stacking listeners.
76
+ *
77
+ * CSS shape (see bundle-base.css):
78
+ * .using-mouse .lb-bundle-widget :focus { outline: none; }
79
+ *
80
+ * The Liquid theme mirrors this behaviour from `bundle-widget.js` against
81
+ * `document.documentElement` so classic and headless storefronts render the
82
+ * same focus rings.
83
+ */
84
+ declare function trackInputMode(target: HTMLElement): () => void;
85
+
86
+ export { LimeBundleElement, trackInputMode };
package/dist/index.d.ts CHANGED
@@ -62,4 +62,25 @@ declare class LimeBundleElement extends HTMLElement {
62
62
  private renderError;
63
63
  }
64
64
 
65
- export { LimeBundleElement };
65
+ /**
66
+ * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a
67
+ * target element so CSS can scope focus styles by the customer's current
68
+ * input device. Default is mouse; the first keyboard-navigation keypress
69
+ * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips
70
+ * to keyboard mode, and the next pointer click flips back.
71
+ *
72
+ * Multiple targets share one pair of document-level listeners — installed
73
+ * on the first `trackInputMode` call, removed when the last target is
74
+ * released. Safe to call across every widget instance on a page without
75
+ * stacking listeners.
76
+ *
77
+ * CSS shape (see bundle-base.css):
78
+ * .using-mouse .lb-bundle-widget :focus { outline: none; }
79
+ *
80
+ * The Liquid theme mirrors this behaviour from `bundle-widget.js` against
81
+ * `document.documentElement` so classic and headless storefronts render the
82
+ * same focus rings.
83
+ */
84
+ declare function trackInputMode(target: HTMLElement): () => void;
85
+
86
+ export { LimeBundleElement, trackInputMode };
package/dist/index.js CHANGED
@@ -212,12 +212,11 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
212
212
  }
213
213
  function buildRowState(bundle, product, productIndex) {
214
214
  const selectedVariantIds = bundle.selectedVariantIds?.[productIndex] ?? null;
215
- const available = product.variants.nodes.filter(
216
- (v) => v.availableForSale
217
- );
218
- const eligibleVariants = selectedVariantIds && selectedVariantIds.length > 0 ? available.filter((v) => selectedVariantIds.includes(v.id)) : available;
219
- const isOos = eligibleVariants.length === 0;
220
- const selected = eligibleVariants[0] ?? null;
215
+ const merchantScoped = selectedVariantIds && selectedVariantIds.length > 0 ? product.variants.nodes.filter((v) => selectedVariantIds.includes(v.id)) : product.variants.nodes;
216
+ const firstInStock = merchantScoped.find((v) => v.availableForSale) ?? null;
217
+ const eligibleVariants = merchantScoped;
218
+ const isOos = !firstInStock;
219
+ const selected = firstInStock ?? merchantScoped[0] ?? null;
221
220
  const qty = selected ? resolveBundleQty(bundle, product.id, selected.id) : 1;
222
221
  return { product, eligibleVariants, selected, qty, isOos };
223
222
  }
@@ -366,33 +365,85 @@ function renderProductRow(state, currency, qtyFor, onVariantChange) {
366
365
  };
367
366
  applyVariantToRow(state.selected);
368
367
  if (state.eligibleVariants.length > 1) {
369
- const select = document.createElement("select");
370
- select.className = "lb-bundle-variant-select";
371
- select.setAttribute("data-variant-select", "");
372
- select.name = `lb-variant-${state.product.id.replace(/^.*\//, "")}`;
373
- select.setAttribute(
374
- "aria-label",
375
- `Select variant for ${state.product.title}`
368
+ const optionNames = state.eligibleVariants[0].selectedOptions.map(
369
+ (o) => o.name
376
370
  );
377
- state.eligibleVariants.forEach((variant) => {
378
- const opt = document.createElement("option");
379
- opt.value = variant.id;
380
- opt.textContent = variant.title;
381
- if (variant.id === state.selected?.id) opt.selected = true;
382
- select.appendChild(opt);
383
- });
384
- select.addEventListener("change", () => {
385
- const variant = state.eligibleVariants.find(
386
- (v) => v.id === select.value
371
+ const productIdTail = state.product.id.replace(/^.*\//, "");
372
+ const optionSelects = [];
373
+ const resolveVariant = (values) => state.eligibleVariants.find(
374
+ (v) => v.selectedOptions.every((o, i) => o.value === values[i]) && v.selectedOptions.length === values.length
375
+ ) ?? null;
376
+ const syncSelectsToVariant = (variant) => {
377
+ variant.selectedOptions.forEach((o, i) => {
378
+ const sel = optionSelects[i];
379
+ if (sel && sel.value !== o.value) sel.value = o.value;
380
+ });
381
+ };
382
+ const isValueAvailable = (optionIndex, value, selected) => state.eligibleVariants.some((v) => {
383
+ if (!v.availableForSale) return false;
384
+ if (v.selectedOptions[optionIndex]?.value !== value) return false;
385
+ return v.selectedOptions.every(
386
+ (o, i) => i === optionIndex || o.value === selected[i]
387
387
  );
388
- if (!variant) return;
388
+ });
389
+ const recomputeDisabled = (selected) => {
390
+ optionSelects.forEach((sel, i) => {
391
+ Array.from(sel.options).forEach((opt) => {
392
+ opt.disabled = !isValueAvailable(i, opt.value, selected);
393
+ });
394
+ });
395
+ };
396
+ const handleChange = () => {
397
+ const values = optionSelects.map((s) => s.value);
398
+ const variant = resolveVariant(values);
399
+ if (!variant) {
400
+ if (state.selected) {
401
+ syncSelectsToVariant(state.selected);
402
+ recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
403
+ }
404
+ return;
405
+ }
389
406
  state.selected = variant;
390
407
  state.qty = qtyFor(state.product.id, variant.id);
391
408
  if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);
392
409
  applyVariantToRow(variant);
410
+ recomputeDisabled(variant.selectedOptions.map((o) => o.value));
393
411
  onVariantChange();
412
+ };
413
+ const groupsContainer = el("div", "lb-bundle-variant-option-groups");
414
+ optionNames.forEach((name2, position) => {
415
+ const group = el("div", "lb-bundle-variant-option-group");
416
+ const label = el("span", "lb-bundle-variant-option-label");
417
+ label.textContent = name2;
418
+ group.appendChild(label);
419
+ const select = document.createElement("select");
420
+ select.className = "lb-bundle-variant-select";
421
+ select.setAttribute("data-variant-option", "");
422
+ select.setAttribute("data-option-position", String(position + 1));
423
+ select.name = `lb-variant-${productIdTail}-${position + 1}`;
424
+ select.setAttribute("aria-label", name2);
425
+ const seen = /* @__PURE__ */ new Set();
426
+ state.eligibleVariants.forEach((v) => {
427
+ const value = v.selectedOptions[position]?.value;
428
+ if (!value || seen.has(value)) return;
429
+ seen.add(value);
430
+ const opt = document.createElement("option");
431
+ opt.value = value;
432
+ opt.textContent = value;
433
+ if (state.selected?.selectedOptions[position]?.value === value) {
434
+ opt.selected = true;
435
+ }
436
+ select.appendChild(opt);
437
+ });
438
+ select.addEventListener("change", handleChange);
439
+ optionSelects.push(select);
440
+ group.appendChild(select);
441
+ groupsContainer.appendChild(group);
394
442
  });
395
- info.appendChild(select);
443
+ info.appendChild(groupsContainer);
444
+ if (state.selected) {
445
+ recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
446
+ }
396
447
  } else if (state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1) {
397
448
  const badge = el("span", "lb-bundle-variant-badge");
398
449
  badge.textContent = state.eligibleVariants[0].title;
@@ -481,6 +532,7 @@ function computeSale(totalCents, discount, rows) {
481
532
  }
482
533
 
483
534
  // src/renderers/mix-match.ts
535
+ import { resolveBundleQty as resolveBundleQty2 } from "@lime-bundles/core";
484
536
  var PLACEHOLDER_THUMB_SVG2 = `
485
537
  <svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
486
538
  <rect x="4" y="4" width="20" height="20" rx="3"></rect>
@@ -548,9 +600,6 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
548
600
  showSearch: wc.showSearch,
549
601
  onAdd: (product, variant) => addSelection(product, variant),
550
602
  onRemove: (productId, variantId) => removeSelection(productId, variantId),
551
- countFor: (productId, variantId) => selections.filter(
552
- (s) => s.productId === productId && s.variantId === variantId
553
- ).length,
554
603
  isOverMax: () => selections.length >= maxQty
555
604
  });
556
605
  root.appendChild(modal.el);
@@ -560,7 +609,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
560
609
  if (cta.disabled) return;
561
610
  const lines = selections.map((s) => ({
562
611
  merchandiseId: s.variantId,
563
- quantity: 1,
612
+ quantity: s.quantity,
564
613
  attributes: [
565
614
  { key: "_lime_bundle_gid", value: bundle.id },
566
615
  { key: "_lime_bundle_type", value: bundle.bundleType }
@@ -594,7 +643,8 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
594
643
  firstVariant.unitPrice,
595
644
  firstVariant.unitPriceMeasurement,
596
645
  currency
597
- )
646
+ ),
647
+ quantity: resolveBundleQty2(bundle, firstEligible.product.id, firstVariant.id)
598
648
  });
599
649
  }
600
650
  afterMutation();
@@ -612,7 +662,8 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
612
662
  variant.unitPrice,
613
663
  variant.unitPriceMeasurement,
614
664
  currency
615
- )
665
+ ),
666
+ quantity: resolveBundleQty2(bundle, product.id, variant.id)
616
667
  });
617
668
  afterMutation();
618
669
  }
@@ -779,6 +830,9 @@ function renderFilledSlot(selection, index, currency, onRemove) {
779
830
  } else {
780
831
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
781
832
  }
833
+ const qtyBadge = el("span", "lb-bundle-qty-badge");
834
+ qtyBadge.textContent = String(selection.quantity);
835
+ thumb.appendChild(qtyBadge);
782
836
  slot.appendChild(thumb);
783
837
  const info = el("div", "lb-mix-match__filled-info");
784
838
  const title = el("span", "lb-mix-match__filled-title");
@@ -789,14 +843,16 @@ function renderFilledSlot(selection, index, currency, onRemove) {
789
843
  variant.textContent = selection.variantTitle;
790
844
  info.appendChild(variant);
791
845
  }
846
+ const linePrice = selection.priceCents * selection.quantity;
847
+ const lineCompare = selection.compareCents !== null ? selection.compareCents * selection.quantity : null;
792
848
  const priceWrap = el("span", "lb-mix-match__filled-price");
793
- if (selection.compareCents && selection.compareCents > selection.priceCents) {
849
+ if (lineCompare !== null && lineCompare > linePrice) {
794
850
  const compare = el("span", "lb-mix-match__filled-compare");
795
- compare.textContent = formatCents(selection.compareCents, currency);
851
+ compare.textContent = formatCents(lineCompare, currency);
796
852
  priceWrap.appendChild(compare);
797
853
  }
798
854
  const priceEl = document.createElement("span");
799
- priceEl.textContent = formatCents(selection.priceCents, currency);
855
+ priceEl.textContent = formatCents(linePrice, currency);
800
856
  priceWrap.appendChild(priceEl);
801
857
  info.appendChild(priceWrap);
802
858
  if (selection.unitPriceLabel) {
@@ -837,7 +893,10 @@ function renderPricingSection(showCompareAtPrice) {
837
893
  return;
838
894
  }
839
895
  wrap.style.display = "";
840
- const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
896
+ const totalCents = selections.reduce(
897
+ (s, sel) => s + sel.priceCents * sel.quantity,
898
+ 0
899
+ );
841
900
  const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
842
901
  if (showCompareAtPrice && totalCents > saleCents) {
843
902
  compare.textContent = formatCents(totalCents, currency);
@@ -864,7 +923,10 @@ function renderSavingsBar2() {
864
923
  wrap.style.display = "none";
865
924
  return;
866
925
  }
867
- const totalCents = selections.reduce((s, sel) => s + sel.priceCents, 0);
926
+ const totalCents = selections.reduce(
927
+ (s, sel) => s + sel.priceCents * sel.quantity,
928
+ 0
929
+ );
868
930
  const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
869
931
  const savings = Math.max(0, totalCents - saleCents);
870
932
  if (savings <= 0) {
@@ -958,8 +1020,8 @@ function renderModal(bundle, eligible, currency, handlers) {
958
1020
  rowsBuilt = true;
959
1021
  list.innerHTML = "";
960
1022
  eligible.forEach((ep) => {
961
- const availableVariants = ep.variants.filter((v) => v.availableForSale);
962
- const firstAvailVariant = availableVariants[0] ?? ep.firstAvailableVariant ?? ep.variants[0];
1023
+ const availableVariants = ep.variants;
1024
+ const firstAvailVariant = ep.variants.find((v) => v.availableForSale) ?? ep.firstAvailableVariant ?? ep.variants[0];
963
1025
  if (!firstAvailVariant) return;
964
1026
  let currentVariant = firstAvailVariant;
965
1027
  const productEl = el(
@@ -984,7 +1046,9 @@ function renderModal(bundle, eligible, currency, handlers) {
984
1046
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
985
1047
  }
986
1048
  const countBadge = el("span", "lb-bundle-qty-badge");
987
- countBadge.style.display = "none";
1049
+ countBadge.textContent = String(
1050
+ resolveBundleQty2(bundle, ep.product.id, currentVariant.id)
1051
+ );
988
1052
  thumb.appendChild(countBadge);
989
1053
  productEl.appendChild(thumb);
990
1054
  const info = el("div", "lb-mix-match__modal-product-info");
@@ -993,7 +1057,7 @@ function renderModal(bundle, eligible, currency, handlers) {
993
1057
  info.appendChild(title);
994
1058
  const price = el("p", "lb-mix-match__modal-product-price");
995
1059
  price.textContent = formatCents(
996
- parseCents(currentVariant.price.amount),
1060
+ parseCents(currentVariant.price.amount) * resolveBundleQty2(bundle, ep.product.id, currentVariant.id),
997
1061
  currency
998
1062
  );
999
1063
  info.appendChild(price);
@@ -1020,28 +1084,53 @@ function renderModal(bundle, eligible, currency, handlers) {
1020
1084
  }
1021
1085
  };
1022
1086
  if (availableVariants.length > 1) {
1023
- const select = document.createElement("select");
1024
- select.className = "lb-mix-match__variant-select";
1025
- select.setAttribute("data-variant-select", "");
1026
- select.name = `lb-variant-${ep.product.id.replace(/^.*\//, "")}`;
1027
- select.setAttribute(
1028
- "aria-label",
1029
- `Select variant for ${ep.product.title}`
1087
+ const optionNames = availableVariants[0].selectedOptions.map(
1088
+ (o) => o.name
1030
1089
  );
1031
- availableVariants.forEach((v) => {
1032
- const opt = document.createElement("option");
1033
- opt.value = v.id;
1034
- opt.textContent = v.title;
1035
- if (v.id === firstAvailVariant.id) opt.selected = true;
1036
- select.appendChild(opt);
1090
+ const productIdTail = ep.product.id.replace(/^.*\//, "");
1091
+ const optionSelects = [];
1092
+ const resolveVariant = (values) => availableVariants.find(
1093
+ (v) => v.selectedOptions.length === values.length && v.selectedOptions.every((o, i) => o.value === values[i])
1094
+ ) ?? null;
1095
+ const syncSelectsToVariant = (v) => {
1096
+ v.selectedOptions.forEach((o, i) => {
1097
+ const sel = optionSelects[i];
1098
+ if (sel && sel.value !== o.value) sel.value = o.value;
1099
+ });
1100
+ };
1101
+ const isValueAvailable = (optionIndex, value, selected) => availableVariants.some((v) => {
1102
+ if (!v.availableForSale) return false;
1103
+ if (v.selectedOptions[optionIndex]?.value !== value) return false;
1104
+ return v.selectedOptions.every(
1105
+ (o, i) => i === optionIndex || o.value === selected[i]
1106
+ );
1037
1107
  });
1038
- select.addEventListener("change", () => {
1039
- const next = availableVariants.find((v) => v.id === select.value);
1040
- if (!next) return;
1108
+ const recomputeDisabled = (selected) => {
1109
+ optionSelects.forEach((sel, i) => {
1110
+ Array.from(sel.options).forEach((opt) => {
1111
+ opt.disabled = !isValueAvailable(i, opt.value, selected);
1112
+ });
1113
+ });
1114
+ };
1115
+ const handleChange = () => {
1116
+ const values = optionSelects.map((s) => s.value);
1117
+ const next = resolveVariant(values);
1118
+ if (!next) {
1119
+ syncSelectsToVariant(currentVariant);
1120
+ recomputeDisabled(
1121
+ currentVariant.selectedOptions.map((o) => o.value)
1122
+ );
1123
+ return;
1124
+ }
1041
1125
  currentVariant = next;
1042
1126
  row.variant = next;
1127
+ const nextQty = resolveBundleQty2(
1128
+ bundle,
1129
+ ep.product.id,
1130
+ currentVariant.id
1131
+ );
1043
1132
  price.textContent = formatCents(
1044
- parseCents(currentVariant.price.amount),
1133
+ parseCents(currentVariant.price.amount) * nextQty,
1045
1134
  currency
1046
1135
  );
1047
1136
  const nextUnitText = formatUnitPrice(
@@ -1056,9 +1145,43 @@ function renderModal(bundle, eligible, currency, handlers) {
1056
1145
  unitPrice.textContent = "";
1057
1146
  unitPrice.hidden = true;
1058
1147
  }
1148
+ recomputeDisabled(next.selectedOptions.map((o) => o.value));
1059
1149
  rowUpdateCount();
1150
+ };
1151
+ const groupsContainer = el("div", "lb-bundle-variant-option-groups");
1152
+ optionNames.forEach((name, position) => {
1153
+ const group = el("div", "lb-bundle-variant-option-group");
1154
+ const label = el("span", "lb-bundle-variant-option-label");
1155
+ label.textContent = name;
1156
+ group.appendChild(label);
1157
+ const select = document.createElement("select");
1158
+ select.className = "lb-mix-match__variant-select";
1159
+ select.setAttribute("data-variant-option", "");
1160
+ select.setAttribute("data-option-position", String(position + 1));
1161
+ select.name = `lb-variant-${productIdTail}-${position + 1}`;
1162
+ select.setAttribute("aria-label", name);
1163
+ const seen = /* @__PURE__ */ new Set();
1164
+ availableVariants.forEach((v) => {
1165
+ const value = v.selectedOptions[position]?.value;
1166
+ if (!value || seen.has(value)) return;
1167
+ seen.add(value);
1168
+ const opt = document.createElement("option");
1169
+ opt.value = value;
1170
+ opt.textContent = value;
1171
+ if (firstAvailVariant.selectedOptions[position]?.value === value) {
1172
+ opt.selected = true;
1173
+ }
1174
+ select.appendChild(opt);
1175
+ });
1176
+ select.addEventListener("change", handleChange);
1177
+ optionSelects.push(select);
1178
+ group.appendChild(select);
1179
+ groupsContainer.appendChild(group);
1060
1180
  });
1061
- info.appendChild(select);
1181
+ info.appendChild(groupsContainer);
1182
+ recomputeDisabled(
1183
+ firstAvailVariant.selectedOptions.map((o) => o.value)
1184
+ );
1062
1185
  } else if (availableVariants.length === 1 && firstAvailVariant.title !== "Default Title") {
1063
1186
  const variantLabel = el("span", "lb-mix-match__filled-variant");
1064
1187
  variantLabel.textContent = firstAvailVariant.title;
@@ -1071,13 +1194,9 @@ function renderModal(bundle, eligible, currency, handlers) {
1071
1194
  }
1072
1195
  productEl.appendChild(info);
1073
1196
  const rowUpdateCount = () => {
1074
- const count = handlers.countFor(ep.product.id, currentVariant.id);
1075
- if (count > 0) {
1076
- countBadge.textContent = String(count);
1077
- countBadge.style.display = "";
1078
- } else {
1079
- countBadge.style.display = "none";
1080
- }
1197
+ countBadge.textContent = String(
1198
+ resolveBundleQty2(bundle, ep.product.id, currentVariant.id)
1199
+ );
1081
1200
  };
1082
1201
  if (!ep.isOos) {
1083
1202
  const addBtn = document.createElement("button");
@@ -1087,6 +1206,7 @@ function renderModal(bundle, eligible, currency, handlers) {
1087
1206
  addBtn.addEventListener("click", () => {
1088
1207
  if (handlers.isOverMax()) return;
1089
1208
  handlers.onAdd(ep.product, currentVariant);
1209
+ close();
1090
1210
  });
1091
1211
  productEl.appendChild(addBtn);
1092
1212
  }
@@ -1492,6 +1612,61 @@ function clamp(n, min, max) {
1492
1612
  // src/lime-bundle.ts
1493
1613
  import { applyWidgetConfigVars } from "@lime-bundles/core";
1494
1614
 
1615
+ // src/utils/input-mode.ts
1616
+ var NAV_KEYS = /* @__PURE__ */ new Set([
1617
+ "Tab",
1618
+ "ArrowUp",
1619
+ "ArrowDown",
1620
+ "ArrowLeft",
1621
+ "ArrowRight",
1622
+ "Home",
1623
+ "End",
1624
+ "PageUp",
1625
+ "PageDown",
1626
+ "Enter",
1627
+ " ",
1628
+ "Escape"
1629
+ ]);
1630
+ var targets = /* @__PURE__ */ new Set();
1631
+ var listenersAttached = false;
1632
+ function setAll(on) {
1633
+ const off = on === "using-mouse" ? "using-keyboard" : "using-mouse";
1634
+ for (const el2 of targets) {
1635
+ el2.classList.add(on);
1636
+ el2.classList.remove(off);
1637
+ }
1638
+ }
1639
+ function onKeyDown(e) {
1640
+ if (NAV_KEYS.has(e.key)) setAll("using-keyboard");
1641
+ }
1642
+ function onPointerDown() {
1643
+ setAll("using-mouse");
1644
+ }
1645
+ function attachListeners() {
1646
+ if (listenersAttached) return;
1647
+ listenersAttached = true;
1648
+ document.addEventListener("keydown", onKeyDown, true);
1649
+ document.addEventListener("pointerdown", onPointerDown, true);
1650
+ }
1651
+ function detachListeners() {
1652
+ if (!listenersAttached) return;
1653
+ listenersAttached = false;
1654
+ document.removeEventListener("keydown", onKeyDown, true);
1655
+ document.removeEventListener("pointerdown", onPointerDown, true);
1656
+ }
1657
+ function trackInputMode(target) {
1658
+ target.classList.add("using-mouse");
1659
+ target.classList.remove("using-keyboard");
1660
+ targets.add(target);
1661
+ attachListeners();
1662
+ return () => {
1663
+ targets.delete(target);
1664
+ target.classList.remove("using-mouse");
1665
+ target.classList.remove("using-keyboard");
1666
+ if (targets.size === 0) detachListeners();
1667
+ };
1668
+ }
1669
+
1495
1670
  // src/styles/bundle-css.ts
1496
1671
  var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle widget types */
1497
1672
 
@@ -1776,6 +1951,56 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1776
1951
  margin-top: 8px;
1777
1952
  }
1778
1953
 
1954
+ /* Per-option variant pickers. Each option's label + select sit inside a
1955
+ .lb-bundle-variant-option-group (flex column, 2px gap between label and
1956
+ select); the groups stack inside a .lb-bundle-variant-option-groups
1957
+ parent (flex column, 12px gap between groups). The parent owns the top
1958
+ offset from the preceding unit-price line, so individual labels and
1959
+ selects don't carry their own vertical margins. */
1960
+ .lb-bundle-variant-option-groups {
1961
+ display: flex;
1962
+ flex-direction: column;
1963
+ gap: 12px;
1964
+ margin-top: 8px;
1965
+ }
1966
+
1967
+ .lb-bundle-variant-option-group {
1968
+ display: flex;
1969
+ flex-direction: column;
1970
+ gap: 2px;
1971
+ }
1972
+
1973
+ .lb-bundle-variant-option-label {
1974
+ display: block;
1975
+ margin: 0;
1976
+ font-size: 12px;
1977
+ line-height: 16px;
1978
+ font-weight: 600;
1979
+ letter-spacing: 0.05em;
1980
+ text-transform: uppercase;
1981
+ color: color-mix(in srgb, var(--lb-text) 60%, transparent);
1982
+ }
1983
+
1984
+ .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {
1985
+ margin-top: 4px;
1986
+ }
1987
+
1988
+ /* Focus-ring suppression for mouse users. A small JS helper \u2014 see
1989
+ packages/widget/src/utils/input-mode.ts and bundle-widget.js \u2014 toggles
1990
+ .using-mouse / .using-keyboard on the widget root (or html in the Liquid
1991
+ path) based on the customer's current input device. Default is mouse, so
1992
+ click-to-focus doesn't leave a keyboard-style ring. The modal overlay
1993
+ gets its own selector because the Liquid path reparents it to body,
1994
+ outside the widget root. */
1995
+ .using-mouse .lb-bundle-widget :focus,
1996
+ .using-mouse .lb-bundle-widget :focus-visible,
1997
+ .using-mouse .lb-mix-match__modal-overlay :focus,
1998
+ .using-mouse .lb-mix-match__modal-overlay :focus-visible {
1999
+ outline: none;
2000
+ outline-offset: 0;
2001
+ box-shadow: none;
2002
+ }
2003
+
1779
2004
  .lb-bundle-quantity {
1780
2005
  font-size: 12px;
1781
2006
  line-height: 16px;
@@ -2033,9 +2258,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2033
2258
  border: none;
2034
2259
  }
2035
2260
 
2036
- /* Variant picker select \u2014 styled to match the variant badge aesthetic */
2261
+ /* Variant picker select \u2014 styled to match the variant badge aesthetic.
2262
+ Sits inside .lb-bundle-variant-option-group so vertical spacing is owned
2263
+ by the group/groups flex gap, not the select itself. */
2037
2264
  .lb-bundle-variant-select {
2038
- margin-top: 8px;
2039
2265
  display: inline-block;
2040
2266
  border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
2041
2267
  border-radius: var(--lb-variant-radius);
@@ -2052,7 +2278,8 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2052
2278
  background-repeat: no-repeat;
2053
2279
  background-position: right 8px center;
2054
2280
  background-size: 12px;
2055
- max-width: 100%;
2281
+ width: 50%;
2282
+ max-width: 50%;
2056
2283
  }
2057
2284
 
2058
2285
  .lb-bundle-variant-select:focus-visible {
@@ -2459,7 +2686,6 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2459
2686
 
2460
2687
  .lb-mix-match__variant-select {
2461
2688
  font-size: 12px;
2462
- margin: 4px 0 0;
2463
2689
  padding: 4px 24px 4px 8px;
2464
2690
  border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
2465
2691
  border-radius: var(--lb-picker-variant-radius);
@@ -2472,7 +2698,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2472
2698
  font-family: inherit;
2473
2699
  min-height: 32px;
2474
2700
  cursor: pointer;
2475
- max-width: 120px;
2701
+ width: 50%;
2702
+ max-width: 50%;
2476
2703
  appearance: none;
2477
2704
  -webkit-appearance: none;
2478
2705
  }
@@ -2556,6 +2783,11 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2556
2783
  .lb-mix-match__modal-overlay--open .lb-mix-match__modal {
2557
2784
  transform: translateY(0);
2558
2785
  }
2786
+
2787
+ .lb-mix-match__variant-select {
2788
+ width: 80%;
2789
+ max-width: 80%;
2790
+ }
2559
2791
  }
2560
2792
 
2561
2793
  /* === Reduced Motion === */
@@ -3129,6 +3361,7 @@ var LimeBundleElement = class extends HTMLElement {
3129
3361
  container.setAttribute("data-bundle-type", bundle.bundleType);
3130
3362
  container.setAttribute("data-bundle-gid", bundle.id);
3131
3363
  applyWidgetConfigVars(container, bundle.widgetConfig);
3364
+ this.renderCleanups.push(trackInputMode(container));
3132
3365
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
3133
3366
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
3134
3367
  switch (bundle.bundleType) {
@@ -3230,6 +3463,7 @@ if (typeof customElements !== "undefined" && !customElements.get("lime-bundle"))
3230
3463
  customElements.define("lime-bundle", LimeBundleElement);
3231
3464
  }
3232
3465
  export {
3233
- LimeBundleElement
3466
+ LimeBundleElement,
3467
+ trackInputMode
3234
3468
  };
3235
3469
  //# sourceMappingURL=index.js.map