@lime-bundles/widget 2.3.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.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,32 +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.setAttribute(
373
- "aria-label",
374
- `Select variant for ${state.product.title}`
368
+ const optionNames = state.eligibleVariants[0].selectedOptions.map(
369
+ (o) => o.name
375
370
  );
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
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]
386
387
  );
387
- 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
+ }
388
406
  state.selected = variant;
389
407
  state.qty = qtyFor(state.product.id, variant.id);
390
408
  if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);
391
409
  applyVariantToRow(variant);
410
+ recomputeDisabled(variant.selectedOptions.map((o) => o.value));
392
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);
393
442
  });
394
- info.appendChild(select);
443
+ info.appendChild(groupsContainer);
444
+ if (state.selected) {
445
+ recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));
446
+ }
395
447
  } else if (state.eligibleVariants.length === 1 && state.product.variants.nodes.length > 1) {
396
448
  const badge = el("span", "lb-bundle-variant-badge");
397
449
  badge.textContent = state.eligibleVariants[0].title;
@@ -480,6 +532,7 @@ function computeSale(totalCents, discount, rows) {
480
532
  }
481
533
 
482
534
  // src/renderers/mix-match.ts
535
+ import { resolveBundleQty as resolveBundleQty2 } from "@lime-bundles/core";
483
536
  var PLACEHOLDER_THUMB_SVG2 = `
484
537
  <svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
485
538
  <rect x="4" y="4" width="20" height="20" rx="3"></rect>
@@ -547,9 +600,6 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
547
600
  showSearch: wc.showSearch,
548
601
  onAdd: (product, variant) => addSelection(product, variant),
549
602
  onRemove: (productId, variantId) => removeSelection(productId, variantId),
550
- countFor: (productId, variantId) => selections.filter(
551
- (s) => s.productId === productId && s.variantId === variantId
552
- ).length,
553
603
  isOverMax: () => selections.length >= maxQty
554
604
  });
555
605
  root.appendChild(modal.el);
@@ -559,7 +609,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
559
609
  if (cta.disabled) return;
560
610
  const lines = selections.map((s) => ({
561
611
  merchandiseId: s.variantId,
562
- quantity: 1,
612
+ quantity: s.quantity,
563
613
  attributes: [
564
614
  { key: "_lime_bundle_gid", value: bundle.id },
565
615
  { key: "_lime_bundle_type", value: bundle.bundleType }
@@ -588,7 +638,13 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
588
638
  variantTitle: firstVariant.title,
589
639
  imageUrl: firstEligible.product.featuredImage?.url ?? null,
590
640
  priceCents: parseCents(firstVariant.price.amount),
591
- compareCents: firstVariant.compareAtPrice ? parseCents(firstVariant.compareAtPrice.amount) : null
641
+ compareCents: firstVariant.compareAtPrice ? parseCents(firstVariant.compareAtPrice.amount) : null,
642
+ unitPriceLabel: formatUnitPrice(
643
+ firstVariant.unitPrice,
644
+ firstVariant.unitPriceMeasurement,
645
+ currency
646
+ ),
647
+ quantity: resolveBundleQty2(bundle, firstEligible.product.id, firstVariant.id)
592
648
  });
593
649
  }
594
650
  afterMutation();
@@ -601,7 +657,13 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
601
657
  variantTitle: variant.title,
602
658
  imageUrl: product.featuredImage?.url ?? null,
603
659
  priceCents: parseCents(variant.price.amount),
604
- compareCents: variant.compareAtPrice ? parseCents(variant.compareAtPrice.amount) : null
660
+ compareCents: variant.compareAtPrice ? parseCents(variant.compareAtPrice.amount) : null,
661
+ unitPriceLabel: formatUnitPrice(
662
+ variant.unitPrice,
663
+ variant.unitPriceMeasurement,
664
+ currency
665
+ ),
666
+ quantity: resolveBundleQty2(bundle, product.id, variant.id)
605
667
  });
606
668
  afterMutation();
607
669
  }
@@ -768,6 +830,9 @@ function renderFilledSlot(selection, index, currency, onRemove) {
768
830
  } else {
769
831
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
770
832
  }
833
+ const qtyBadge = el("span", "lb-bundle-qty-badge");
834
+ qtyBadge.textContent = String(selection.quantity);
835
+ thumb.appendChild(qtyBadge);
771
836
  slot.appendChild(thumb);
772
837
  const info = el("div", "lb-mix-match__filled-info");
773
838
  const title = el("span", "lb-mix-match__filled-title");
@@ -778,16 +843,23 @@ function renderFilledSlot(selection, index, currency, onRemove) {
778
843
  variant.textContent = selection.variantTitle;
779
844
  info.appendChild(variant);
780
845
  }
846
+ const linePrice = selection.priceCents * selection.quantity;
847
+ const lineCompare = selection.compareCents !== null ? selection.compareCents * selection.quantity : null;
781
848
  const priceWrap = el("span", "lb-mix-match__filled-price");
782
- if (selection.compareCents && selection.compareCents > selection.priceCents) {
849
+ if (lineCompare !== null && lineCompare > linePrice) {
783
850
  const compare = el("span", "lb-mix-match__filled-compare");
784
- compare.textContent = formatCents(selection.compareCents, currency);
851
+ compare.textContent = formatCents(lineCompare, currency);
785
852
  priceWrap.appendChild(compare);
786
853
  }
787
854
  const priceEl = document.createElement("span");
788
- priceEl.textContent = formatCents(selection.priceCents, currency);
855
+ priceEl.textContent = formatCents(linePrice, currency);
789
856
  priceWrap.appendChild(priceEl);
790
857
  info.appendChild(priceWrap);
858
+ if (selection.unitPriceLabel) {
859
+ const unitPrice = el("span", "lb-bundle-product-unit-price");
860
+ unitPrice.textContent = selection.unitPriceLabel;
861
+ info.appendChild(unitPrice);
862
+ }
791
863
  slot.appendChild(info);
792
864
  const remove = document.createElement("button");
793
865
  remove.type = "button";
@@ -821,7 +893,10 @@ function renderPricingSection(showCompareAtPrice) {
821
893
  return;
822
894
  }
823
895
  wrap.style.display = "";
824
- 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
+ );
825
900
  const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
826
901
  if (showCompareAtPrice && totalCents > saleCents) {
827
902
  compare.textContent = formatCents(totalCents, currency);
@@ -848,7 +923,10 @@ function renderSavingsBar2() {
848
923
  wrap.style.display = "none";
849
924
  return;
850
925
  }
851
- 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
+ );
852
930
  const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);
853
931
  const savings = Math.max(0, totalCents - saleCents);
854
932
  if (savings <= 0) {
@@ -942,8 +1020,10 @@ function renderModal(bundle, eligible, currency, handlers) {
942
1020
  rowsBuilt = true;
943
1021
  list.innerHTML = "";
944
1022
  eligible.forEach((ep) => {
945
- const variant = ep.firstAvailableVariant ?? ep.variants[0];
946
- if (!variant) return;
1023
+ const availableVariants = ep.variants;
1024
+ const firstAvailVariant = ep.variants.find((v) => v.availableForSale) ?? ep.firstAvailableVariant ?? ep.variants[0];
1025
+ if (!firstAvailVariant) return;
1026
+ let currentVariant = firstAvailVariant;
947
1027
  const productEl = el(
948
1028
  "div",
949
1029
  ep.isOos ? "lb-mix-match__modal-product lb-mix-match__modal-product--sold-out" : "lb-mix-match__modal-product",
@@ -965,26 +1045,147 @@ function renderModal(bundle, eligible, currency, handlers) {
965
1045
  } else {
966
1046
  thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
967
1047
  }
1048
+ const countBadge = el("span", "lb-bundle-qty-badge");
1049
+ countBadge.textContent = String(
1050
+ resolveBundleQty2(bundle, ep.product.id, currentVariant.id)
1051
+ );
1052
+ thumb.appendChild(countBadge);
968
1053
  productEl.appendChild(thumb);
969
1054
  const info = el("div", "lb-mix-match__modal-product-info");
970
- const title = el("span", "lb-mix-match__modal-product-title");
1055
+ const title = el("p", "lb-mix-match__modal-product-title");
971
1056
  title.textContent = ep.product.title;
972
1057
  info.appendChild(title);
973
- const price = el("span", "lb-mix-match__modal-product-price");
974
- price.textContent = formatCents(parseCents(variant.price.amount), currency);
1058
+ const price = el("p", "lb-mix-match__modal-product-price");
1059
+ price.textContent = formatCents(
1060
+ parseCents(currentVariant.price.amount) * resolveBundleQty2(bundle, ep.product.id, currentVariant.id),
1061
+ currency
1062
+ );
975
1063
  info.appendChild(price);
976
- const unitPriceText = formatUnitPrice(
977
- variant.unitPrice,
978
- variant.unitPriceMeasurement,
1064
+ const unitPrice = el(
1065
+ "p",
1066
+ "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price"
1067
+ );
1068
+ const initialUnitText = formatUnitPrice(
1069
+ currentVariant.unitPrice,
1070
+ currentVariant.unitPriceMeasurement,
979
1071
  currency
980
1072
  );
981
- if (unitPriceText) {
982
- const unitPrice = el(
983
- "span",
984
- "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price"
1073
+ if (initialUnitText) {
1074
+ unitPrice.textContent = initialUnitText;
1075
+ } else {
1076
+ unitPrice.hidden = true;
1077
+ }
1078
+ info.appendChild(unitPrice);
1079
+ const row = {
1080
+ el: productEl,
1081
+ product: ep.product,
1082
+ variant: firstAvailVariant,
1083
+ updateCount: () => {
1084
+ }
1085
+ };
1086
+ if (availableVariants.length > 1) {
1087
+ const optionNames = availableVariants[0].selectedOptions.map(
1088
+ (o) => o.name
985
1089
  );
986
- unitPrice.textContent = unitPriceText;
987
- info.appendChild(unitPrice);
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
+ );
1107
+ });
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
+ }
1125
+ currentVariant = next;
1126
+ row.variant = next;
1127
+ const nextQty = resolveBundleQty2(
1128
+ bundle,
1129
+ ep.product.id,
1130
+ currentVariant.id
1131
+ );
1132
+ price.textContent = formatCents(
1133
+ parseCents(currentVariant.price.amount) * nextQty,
1134
+ currency
1135
+ );
1136
+ const nextUnitText = formatUnitPrice(
1137
+ currentVariant.unitPrice,
1138
+ currentVariant.unitPriceMeasurement,
1139
+ currency
1140
+ );
1141
+ if (nextUnitText) {
1142
+ unitPrice.textContent = nextUnitText;
1143
+ unitPrice.hidden = false;
1144
+ } else {
1145
+ unitPrice.textContent = "";
1146
+ unitPrice.hidden = true;
1147
+ }
1148
+ recomputeDisabled(next.selectedOptions.map((o) => o.value));
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);
1180
+ });
1181
+ info.appendChild(groupsContainer);
1182
+ recomputeDisabled(
1183
+ firstAvailVariant.selectedOptions.map((o) => o.value)
1184
+ );
1185
+ } else if (availableVariants.length === 1 && firstAvailVariant.title !== "Default Title") {
1186
+ const variantLabel = el("span", "lb-mix-match__filled-variant");
1187
+ variantLabel.textContent = firstAvailVariant.title;
1188
+ info.appendChild(variantLabel);
988
1189
  }
989
1190
  if (ep.isOos) {
990
1191
  const soldOut = el("span", "lb-mix-match__modal-sold-out-label");
@@ -992,56 +1193,26 @@ function renderModal(bundle, eligible, currency, handlers) {
992
1193
  info.appendChild(soldOut);
993
1194
  }
994
1195
  productEl.appendChild(info);
1196
+ const rowUpdateCount = () => {
1197
+ countBadge.textContent = String(
1198
+ resolveBundleQty2(bundle, ep.product.id, currentVariant.id)
1199
+ );
1200
+ };
995
1201
  if (!ep.isOos) {
996
1202
  const addBtn = document.createElement("button");
997
1203
  addBtn.type = "button";
998
1204
  addBtn.className = "lb-mix-match__modal-add";
999
- addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
1000
- addBtn.innerHTML = PLUS_ICON_SVG;
1001
- const countBadge = el("span", "lb-bundle-qty-badge");
1002
- countBadge.style.display = "none";
1003
- addBtn.appendChild(countBadge);
1205
+ addBtn.textContent = "Add";
1004
1206
  addBtn.addEventListener("click", () => {
1005
1207
  if (handlers.isOverMax()) return;
1006
- handlers.onAdd(ep.product, variant);
1208
+ handlers.onAdd(ep.product, currentVariant);
1209
+ close();
1007
1210
  });
1008
1211
  productEl.appendChild(addBtn);
1009
- const removeBtn = document.createElement("button");
1010
- removeBtn.type = "button";
1011
- removeBtn.className = "lb-mix-match__slot-remove";
1012
- removeBtn.setAttribute("aria-label", `Remove one ${ep.product.title}`);
1013
- removeBtn.style.display = "none";
1014
- removeBtn.innerHTML = CLOSE_ICON_SVG;
1015
- removeBtn.addEventListener("click", (e) => {
1016
- e.stopPropagation();
1017
- handlers.onRemove(ep.product.id, variant.id);
1018
- });
1019
- productEl.appendChild(removeBtn);
1020
- productRows.push({
1021
- el: productEl,
1022
- product: ep.product,
1023
- variant,
1024
- updateCount: () => {
1025
- const count = handlers.countFor(ep.product.id, variant.id);
1026
- if (count > 0) {
1027
- countBadge.textContent = String(count);
1028
- countBadge.style.display = "";
1029
- removeBtn.style.display = "";
1030
- } else {
1031
- countBadge.style.display = "none";
1032
- removeBtn.style.display = "none";
1033
- }
1034
- }
1035
- });
1036
- } else {
1037
- productRows.push({
1038
- el: productEl,
1039
- product: ep.product,
1040
- variant,
1041
- updateCount: () => {
1042
- }
1043
- });
1044
1212
  }
1213
+ row.updateCount = ep.isOos ? () => {
1214
+ } : rowUpdateCount;
1215
+ productRows.push(row);
1045
1216
  list.appendChild(productEl);
1046
1217
  });
1047
1218
  refreshCounts();
@@ -1441,6 +1612,61 @@ function clamp(n, min, max) {
1441
1612
  // src/lime-bundle.ts
1442
1613
  import { applyWidgetConfigVars } from "@lime-bundles/core";
1443
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
+
1444
1670
  // src/styles/bundle-css.ts
1445
1671
  var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle widget types */
1446
1672
 
@@ -1612,15 +1838,18 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1612
1838
  /* Product rows */
1613
1839
  .lb-bundle-product-row {
1614
1840
  display: flex;
1615
- gap: 12px;
1841
+ gap: 20px;
1616
1842
  padding: 12px 0;
1617
1843
  }
1618
1844
 
1619
1845
  .lb-bundle-thumbnail {
1620
1846
  position: relative;
1621
1847
  width: 48px;
1622
- height: 48px;
1623
1848
  min-width: 48px;
1849
+ /* Aspect-ratio comes from the merchant \`thumbnailRatio\` enum via Liquid;
1850
+ "original" sets it to \`auto\` so the box sizes to the image's intrinsic
1851
+ ratio. Default keeps the historical 1:1 behaviour. */
1852
+ aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);
1624
1853
  background: var(--lb-thumbnail-bg);
1625
1854
  border-radius: 8px;
1626
1855
  overflow: hidden;
@@ -1631,8 +1860,10 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1631
1860
 
1632
1861
  .lb-bundle-thumbnail img {
1633
1862
  width: 100%;
1634
- height: 100%;
1635
- object-fit: cover;
1863
+ /* Height + fit come from the same merchant enum; "original" sets them to
1864
+ \`auto\` / \`contain\` so the image renders uncropped at intrinsic ratio. */
1865
+ height: var(--lb-thumbnail-img-height, 100%);
1866
+ object-fit: var(--lb-thumbnail-img-fit, cover);
1636
1867
  }
1637
1868
 
1638
1869
  .lb-bundle-thumbnail svg {
@@ -1663,7 +1894,7 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1663
1894
  .lb-bundle-product-price {
1664
1895
  /* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
1665
1896
  display: var(--lb-product-price-display, block);
1666
- font-size: 12px;
1897
+ font-size: 14px;
1667
1898
  font-weight: 400;
1668
1899
  line-height: 20px;
1669
1900
  color: var(--lb-text);
@@ -1680,7 +1911,7 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1680
1911
  .lb-bundle-product-compare-price {
1681
1912
  /* --lb-product-compare-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
1682
1913
  display: var(--lb-product-compare-display, inline);
1683
- font-size: 12px;
1914
+ font-size: 14px;
1684
1915
  font-weight: 400;
1685
1916
  line-height: 20px;
1686
1917
  color: color-mix(in srgb, var(--lb-text) 60%, transparent);
@@ -1699,7 +1930,7 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1699
1930
  secondary text beneath the price row so it doesn't compete visually. */
1700
1931
  .lb-bundle-product-unit-price {
1701
1932
  display: block;
1702
- font-size: 11px;
1933
+ font-size: 12px;
1703
1934
  line-height: 16px;
1704
1935
  color: color-mix(in srgb, var(--lb-text) 60%, transparent);
1705
1936
  margin-top: 2px;
@@ -1720,6 +1951,56 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1720
1951
  margin-top: 8px;
1721
1952
  }
1722
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
+
1723
2004
  .lb-bundle-quantity {
1724
2005
  font-size: 12px;
1725
2006
  line-height: 16px;
@@ -1822,7 +2103,7 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
1822
2103
  grid-template-rows: 1fr;
1823
2104
  grid-template-columns: 1fr;
1824
2105
  width: 100%;
1825
- padding: 16px;
2106
+ padding: 12px 16px;
1826
2107
  border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);
1827
2108
  border-radius: var(--lb-cta-radius);
1828
2109
  font-size: 16px;
@@ -1957,14 +2238,14 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
1957
2238
 
1958
2239
  /* Fixed bundles: product rows */
1959
2240
  .lb-fixed .lb-bundle-product-row {
1960
- gap: 16px;
2241
+ gap: 20px;
1961
2242
  align-items: center;
1962
2243
  }
1963
2244
 
1964
- /* Fixed bundles: larger thumbnails */
2245
+ /* Fixed bundles: larger thumbnails. Height comes from --lb-thumbnail-aspect-ratio
2246
+ (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */
1965
2247
  .lb-fixed .lb-bundle-thumbnail {
1966
2248
  width: 60px;
1967
- height: 60px;
1968
2249
  min-width: 60px;
1969
2250
  border-radius: var(--lb-image-border-radius);
1970
2251
  border: var(--lb-image-border-width) solid var(--lb-image-border-color);
@@ -1977,9 +2258,10 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
1977
2258
  border: none;
1978
2259
  }
1979
2260
 
1980
- /* 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. */
1981
2264
  .lb-bundle-variant-select {
1982
- margin-top: 8px;
1983
2265
  display: inline-block;
1984
2266
  border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);
1985
2267
  border-radius: var(--lb-variant-radius);
@@ -1996,7 +2278,8 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
1996
2278
  background-repeat: no-repeat;
1997
2279
  background-position: right 8px center;
1998
2280
  background-size: 12px;
1999
- max-width: 100%;
2281
+ width: 50%;
2282
+ max-width: 50%;
2000
2283
  }
2001
2284
 
2002
2285
  .lb-bundle-variant-select:focus-visible {
@@ -2065,7 +2348,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2065
2348
  width: 60px;
2066
2349
  height: 60px;
2067
2350
  min-width: 60px;
2068
- border: var(--lb-image-border-width) dashed color-mix(in srgb, var(--lb-text) 20%, transparent);
2351
+ /* 2px border is intentionally independent of --lb-image-border-width \u2014
2352
+ empty slots always need a visible dashed outline as an affordance,
2353
+ regardless of how the merchant has styled populated thumbnails. */
2354
+ border: 2px dashed color-mix(in srgb, var(--lb-text) 35%, transparent);
2069
2355
  border-radius: var(--lb-image-border-radius);
2070
2356
  box-sizing: border-box;
2071
2357
  display: flex;
@@ -2085,9 +2371,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2085
2371
  cursor: default;
2086
2372
  }
2087
2373
 
2374
+ /* Mix-match thumbnails. Height comes from --lb-thumbnail-aspect-ratio
2375
+ (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */
2088
2376
  .lb-mix-match .lb-bundle-thumbnail {
2089
2377
  width: 60px;
2090
- height: 60px;
2091
2378
  min-width: 60px;
2092
2379
  border-radius: var(--lb-image-border-radius);
2093
2380
  border: var(--lb-image-border-width) solid var(--lb-image-border-color);
@@ -2322,7 +2609,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2322
2609
  .lb-mix-match__modal-product {
2323
2610
  display: flex;
2324
2611
  align-items: center;
2325
- gap: 12px;
2612
+ gap: 20px;
2326
2613
  padding: 12px 0;
2327
2614
  border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 7%, transparent);
2328
2615
  }
@@ -2334,8 +2621,11 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2334
2621
  .lb-mix-match__modal-product-thumb {
2335
2622
  position: relative;
2336
2623
  width: 48px;
2337
- height: 48px;
2338
2624
  min-width: 48px;
2625
+ /* Modal picker thumbs follow the merchant's pickerThumbnailRatio \u2014
2626
+ independent from the main widget's thumbnailRatio so a merchant can
2627
+ e.g. show tall picker thumbs with square main thumbs. */
2628
+ aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);
2339
2629
  border-radius: var(--lb-picker-product-radius);
2340
2630
  border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);
2341
2631
  box-sizing: border-box;
@@ -2356,8 +2646,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2356
2646
 
2357
2647
  .lb-mix-match__modal-product-thumb img {
2358
2648
  width: 100%;
2359
- height: 100%;
2360
- object-fit: cover;
2649
+ /* Height + fit come from pickerThumbnailRatio \u2014 "original" sets both
2650
+ to auto/contain so the image renders uncropped at intrinsic ratio. */
2651
+ height: var(--lb-picker-thumbnail-img-height, 100%);
2652
+ object-fit: var(--lb-picker-thumbnail-img-fit, cover);
2361
2653
  border-radius: max(0px, calc(var(--lb-picker-product-radius) - var(--lb-picker-product-border-width)));
2362
2654
  }
2363
2655
 
@@ -2394,7 +2686,6 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2394
2686
 
2395
2687
  .lb-mix-match__variant-select {
2396
2688
  font-size: 12px;
2397
- margin: 4px 0 0;
2398
2689
  padding: 4px 24px 4px 8px;
2399
2690
  border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
2400
2691
  border-radius: var(--lb-picker-variant-radius);
@@ -2407,7 +2698,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2407
2698
  font-family: inherit;
2408
2699
  min-height: 32px;
2409
2700
  cursor: pointer;
2410
- max-width: 120px;
2701
+ width: 50%;
2702
+ max-width: 50%;
2411
2703
  appearance: none;
2412
2704
  -webkit-appearance: none;
2413
2705
  }
@@ -2491,6 +2783,11 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
2491
2783
  .lb-mix-match__modal-overlay--open .lb-mix-match__modal {
2492
2784
  transform: translateY(0);
2493
2785
  }
2786
+
2787
+ .lb-mix-match__variant-select {
2788
+ width: 80%;
2789
+ max-width: 80%;
2790
+ }
2494
2791
  }
2495
2792
 
2496
2793
  /* === Reduced Motion === */
@@ -2592,7 +2889,7 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2592
2889
 
2593
2890
  .lb-volume__tier-price {
2594
2891
  grid-column: 1 / -1;
2595
- font-size: 12px;
2892
+ font-size: 14px;
2596
2893
  font-weight: 500;
2597
2894
  color: var(--lb-text);
2598
2895
  }
@@ -2604,7 +2901,7 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
2604
2901
 
2605
2902
  /* Compare-at (strikethrough) price */
2606
2903
  .lb-volume__tier-compare {
2607
- font-size: 12px;
2904
+ font-size: 14px;
2608
2905
  line-height: 16px;
2609
2906
  color: color-mix(in srgb, var(--lb-text) 60%, transparent);
2610
2907
  text-decoration: line-through;
@@ -3064,6 +3361,7 @@ var LimeBundleElement = class extends HTMLElement {
3064
3361
  container.setAttribute("data-bundle-type", bundle.bundleType);
3065
3362
  container.setAttribute("data-bundle-gid", bundle.id);
3066
3363
  applyWidgetConfigVars(container, bundle.widgetConfig);
3364
+ this.renderCleanups.push(trackInputMode(container));
3067
3365
  const dispatch = (lines) => this.handleAddToCart(bundle, lines);
3068
3366
  const registerCleanup = (fn) => this.renderCleanups.push(fn);
3069
3367
  switch (bundle.bundleType) {
@@ -3165,6 +3463,7 @@ if (typeof customElements !== "undefined" && !customElements.get("lime-bundle"))
3165
3463
  customElements.define("lime-bundle", LimeBundleElement);
3166
3464
  }
3167
3465
  export {
3168
- LimeBundleElement
3466
+ LimeBundleElement,
3467
+ trackInputMode
3169
3468
  };
3170
3469
  //# sourceMappingURL=index.js.map