@lime-bundles/widget 4.6.5 → 4.6.7

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
@@ -46,6 +46,121 @@ function bundleLineAttributes(bundleGid, bundleType) {
46
46
  ];
47
47
  }
48
48
 
49
+ // ../render/src/cart.ts
50
+ var CART_STOCK_WARNING_CODES = [
51
+ "MERCHANDISE_NOT_ENOUGH_STOCK",
52
+ "MERCHANDISE_OUT_OF_STOCK"
53
+ ];
54
+ function isStockWarning(warning) {
55
+ return !!warning.code && CART_STOCK_WARNING_CODES.indexOf(warning.code) !== -1;
56
+ }
57
+ function hasStockWarning(warnings) {
58
+ const list = warnings || [];
59
+ for (let i = 0; i < list.length; i++) {
60
+ if (isStockWarning(list[i])) return true;
61
+ }
62
+ return false;
63
+ }
64
+ function stockWarningMessage(warnings) {
65
+ const list = warnings || [];
66
+ for (let i = 0; i < list.length; i++) {
67
+ const warning = list[i];
68
+ if (isStockWarning(warning) && warning.message) return warning.message;
69
+ }
70
+ return void 0;
71
+ }
72
+ function totalPerVariant(lines) {
73
+ const totals = /* @__PURE__ */ Object.create(null);
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const line = lines[i];
76
+ totals[line.variantId] = (totals[line.variantId] || 0) + (line.quantity || 0);
77
+ }
78
+ return totals;
79
+ }
80
+ function bundleAddFellShort(requested, before, after) {
81
+ const wanted = totalPerVariant(requested);
82
+ const had = totalPerVariant(before);
83
+ const has = totalPerVariant(after);
84
+ for (const variantId in wanted) {
85
+ if ((has[variantId] || 0) - (had[variantId] || 0) < wanted[variantId]) {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ // ../render/src/strings.ts
93
+ var DEFAULT_STRINGS = {
94
+ locale: "en",
95
+ addItem: "Add",
96
+ addedItem: "Added",
97
+ removeItem: "Remove",
98
+ swapItem: "Swap",
99
+ required: "Required",
100
+ soldOut: "Sold out",
101
+ outOfStock: "Out of stock",
102
+ itemsOutOfStock: {
103
+ one: "__COUNT__ item out of stock",
104
+ other: "__COUNT__ items out of stock"
105
+ },
106
+ cartStockError: "We couldn't add this bundle to your cart. __MESSAGE__",
107
+ quantity: "Quantity",
108
+ increaseQuantity: "Increase quantity",
109
+ decreaseQuantity: "Decrease quantity",
110
+ allTypes: "All",
111
+ editSelection: "Edit selection",
112
+ addToCart: "Add to cart",
113
+ bundlePrice: "Bundle total",
114
+ save: "Save",
115
+ complete: "Complete",
116
+ ofSelected: "__COUNT__ of __TOTAL__ added",
117
+ moreToGo: "__COUNT__ more to go",
118
+ nProductsShown: "__COUNT__ products shown",
119
+ bundleComplete: "Your bundle is complete",
120
+ chooseMoreUnlock: "Choose __COUNT__ more to unlock __PCT__% off",
121
+ chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
122
+ stepOf: "Step __STEP__ of __TOTAL__",
123
+ stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
124
+ doorChoose: "Choose your products",
125
+ doorContinue: "Continue building",
126
+ doorQuickSteps: {
127
+ one: "__COUNT__ quick step",
128
+ other: "__COUNT__ quick steps"
129
+ },
130
+ doorStepsToGo: {
131
+ one: "__COUNT__ step to go",
132
+ other: "__COUNT__ steps to go"
133
+ },
134
+ each: " each",
135
+ buyQty: "Buy __COUNT__",
136
+ itemCount: {
137
+ one: "__COUNT__ item",
138
+ other: "__COUNT__ items"
139
+ },
140
+ needsSpots: {
141
+ one: "Needs __COUNT__ spot",
142
+ other: "Needs __COUNT__ spots"
143
+ }
144
+ };
145
+ function formatItemsOutOfStock(t, count) {
146
+ const raw = t.itemsOutOfStock;
147
+ let tpl;
148
+ if (raw && typeof raw === "object") {
149
+ const form = new Intl.PluralRules(t.locale || "en").select(count);
150
+ tpl = raw[form] || raw.other;
151
+ } else {
152
+ tpl = raw;
153
+ }
154
+ return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
155
+ }
156
+ var CART_STOCK_ERROR_FALLBACK = "We couldn't add this bundle to your cart. __MESSAGE__";
157
+ function formatCartStockError(template, message) {
158
+ const tpl = (template || "").trim() || CART_STOCK_ERROR_FALLBACK;
159
+ const warning = (message || "").trim();
160
+ const composed = tpl.indexOf("__MESSAGE__") === -1 ? tpl + " " + warning : tpl.split("__MESSAGE__").join(warning);
161
+ return composed.replace(/\s+/g, " ").trim();
162
+ }
163
+
49
164
  // ../render/src/adapt.ts
50
165
  import {
51
166
  formatUnitPrice,
@@ -380,6 +495,55 @@ function buildOptionGroups(container, product, selected) {
380
495
  }
381
496
  }
382
497
 
498
+ // ../render/src/product/placeholder.ts
499
+ var SVG_NS = "http://www.w3.org/2000/svg";
500
+ var PLACEHOLDER_ATTR = "data-lb-placeholder";
501
+ var PLACEHOLDER_CLASS = "lb-thumb--placeholder";
502
+ function builtInMark() {
503
+ const svg = document.createElementNS(SVG_NS, "svg");
504
+ svg.setAttribute("class", "lb-bundle-placeholder-icon");
505
+ svg.setAttribute("viewBox", "0 0 48 48");
506
+ const rect = document.createElementNS(SVG_NS, "rect");
507
+ rect.setAttribute("x", "15");
508
+ rect.setAttribute("y", "15");
509
+ rect.setAttribute("width", "18");
510
+ rect.setAttribute("height", "18");
511
+ rect.setAttribute("rx", "2");
512
+ svg.appendChild(rect);
513
+ const dot = document.createElementNS(SVG_NS, "circle");
514
+ dot.setAttribute("cx", "20.5");
515
+ dot.setAttribute("cy", "20.5");
516
+ dot.setAttribute("r", "1.5");
517
+ svg.appendChild(dot);
518
+ const ridge = document.createElementNS(SVG_NS, "path");
519
+ ridge.setAttribute("d", "M33 27l-5-5L17 33");
520
+ svg.appendChild(ridge);
521
+ return svg;
522
+ }
523
+ var make = builtInMark;
524
+ function mark() {
525
+ const node = make();
526
+ const el2 = node.nodeType === 1 ? node : null;
527
+ if (el2) {
528
+ el2.setAttribute(PLACEHOLDER_ATTR, "");
529
+ el2.setAttribute("aria-hidden", "true");
530
+ el2.setAttribute("focusable", "false");
531
+ }
532
+ return node;
533
+ }
534
+ function showThumbnailPlaceholder(box2) {
535
+ const img = box2.querySelector("img");
536
+ if (img) img.remove();
537
+ box2.classList.add(PLACEHOLDER_CLASS);
538
+ if (box2.querySelector("[" + PLACEHOLDER_ATTR + "]")) return;
539
+ box2.appendChild(mark());
540
+ }
541
+ function hideThumbnailPlaceholder(box2) {
542
+ const existing = box2.querySelector("[" + PLACEHOLDER_ATTR + "]");
543
+ if (existing) existing.remove();
544
+ box2.classList.remove(PLACEHOLDER_CLASS);
545
+ }
546
+
383
547
  // ../render/src/product/row.ts
384
548
  var productImgSrc = /* @__PURE__ */ new WeakMap();
385
549
  function updateThumbnail(row, variant, productImage) {
@@ -390,8 +554,12 @@ function updateThumbnail(row, variant, productImage) {
390
554
  const seed = productImage || (img ? img.src : null);
391
555
  if (seed) productImgSrc.set(row, seed);
392
556
  }
393
- const nextSrc = variant.image || productImgSrc.get(row) || null;
394
- if (!nextSrc) return;
557
+ const nextSrc = variant && variant.image || productImgSrc.get(row) || null;
558
+ if (!nextSrc) {
559
+ showThumbnailPlaceholder(thumb);
560
+ return;
561
+ }
562
+ hideThumbnailPlaceholder(thumb);
395
563
  if (img) {
396
564
  img.src = nextSrc;
397
565
  img.srcset = "";
@@ -410,7 +578,10 @@ function updateInlineQuantity(row, qty) {
410
578
  el2.textContent = "\xD7" + qty;
411
579
  }
412
580
  function applyVariant(row, variant, product, formatMoney) {
413
- if (!variant) return;
581
+ if (!variant) {
582
+ updateThumbnail(row, null, product.featuredImage);
583
+ return;
584
+ }
414
585
  updateThumbnail(row, variant, product.featuredImage);
415
586
  updateInlineQuantity(row, variant.quantity || product.quantity || 1);
416
587
  const priceEl = row.querySelector("[data-product-price]");
@@ -460,8 +631,11 @@ function buildRowSkeleton(product, t, opts) {
460
631
  if (opts.bogoRole) row.setAttribute("data-bogo-role", opts.bogoRole);
461
632
  const thumb = box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
462
633
  const ratio = product.featuredImageRatio;
463
- if (opts.thumbnailRatio === "original" && ratio != null && ratio > 0) {
464
- thumb.style.setProperty("--lb-row-thumb-ratio", String(ratio));
634
+ if (opts.thumbnailRatio === "original") {
635
+ thumb.style.setProperty(
636
+ "--lb-row-thumb-ratio",
637
+ String(ratio != null && ratio > 0 ? ratio : 1)
638
+ );
465
639
  }
466
640
  row.appendChild(thumb);
467
641
  const info = box("div", "lb-bundle-product-info");
@@ -743,70 +917,6 @@ function initCountdown(container) {
743
917
  }
744
918
  }
745
919
 
746
- // ../render/src/strings.ts
747
- var DEFAULT_STRINGS = {
748
- locale: "en",
749
- addItem: "Add",
750
- addedItem: "Added",
751
- removeItem: "Remove",
752
- swapItem: "Swap",
753
- required: "Required",
754
- soldOut: "Sold out",
755
- outOfStock: "Out of stock",
756
- itemsOutOfStock: {
757
- one: "__COUNT__ item out of stock",
758
- other: "__COUNT__ items out of stock"
759
- },
760
- quantity: "Quantity",
761
- increaseQuantity: "Increase quantity",
762
- decreaseQuantity: "Decrease quantity",
763
- allTypes: "All",
764
- editSelection: "Edit selection",
765
- addToCart: "Add to cart",
766
- bundlePrice: "Bundle total",
767
- save: "Save",
768
- complete: "Complete",
769
- ofSelected: "__COUNT__ of __TOTAL__ added",
770
- moreToGo: "__COUNT__ more to go",
771
- nProductsShown: "__COUNT__ products shown",
772
- bundleComplete: "Your bundle is complete",
773
- chooseMoreUnlock: "Choose __COUNT__ more to unlock __PCT__% off",
774
- chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
775
- stepOf: "Step __STEP__ of __TOTAL__",
776
- stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
777
- doorChoose: "Choose your products",
778
- doorContinue: "Continue building",
779
- doorQuickSteps: {
780
- one: "__COUNT__ quick step",
781
- other: "__COUNT__ quick steps"
782
- },
783
- doorStepsToGo: {
784
- one: "__COUNT__ step to go",
785
- other: "__COUNT__ steps to go"
786
- },
787
- each: " each",
788
- buyQty: "Buy __COUNT__",
789
- itemCount: {
790
- one: "__COUNT__ item",
791
- other: "__COUNT__ items"
792
- },
793
- needsSpots: {
794
- one: "Needs __COUNT__ spot",
795
- other: "Needs __COUNT__ spots"
796
- }
797
- };
798
- function formatItemsOutOfStock(t, count) {
799
- const raw = t.itemsOutOfStock;
800
- let tpl;
801
- if (raw && typeof raw === "object") {
802
- const form = new Intl.PluralRules(t.locale || "en").select(count);
803
- tpl = raw[form] || raw.other;
804
- } else {
805
- tpl = raw;
806
- }
807
- return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
808
- }
809
-
810
920
  // ../render/src/dropdown/bind.ts
811
921
  import { dropdown } from "@lime-bundles/core";
812
922
 
@@ -1426,17 +1536,25 @@ function buildPickerRow(p, rowIndex, ctx) {
1426
1536
  addedBadge.textContent = t.addedItem || "Added";
1427
1537
  addedBadge.hidden = true;
1428
1538
  thumbDiv.appendChild(addedBadge);
1539
+ const paintThumb = (src) => {
1540
+ if (!src) {
1541
+ showThumbnailPlaceholder(thumbDiv);
1542
+ return;
1543
+ }
1544
+ hideThumbnailPlaceholder(thumbDiv);
1545
+ let img = thumbDiv.querySelector("img");
1546
+ if (!img) {
1547
+ img = document.createElement("img");
1548
+ img.sizes = PICKER_SIZES;
1549
+ img.alt = p.title;
1550
+ img.loading = "lazy";
1551
+ thumbDiv.appendChild(img);
1552
+ }
1553
+ img.src = imgWidth(src, PICKER_BASE_WIDTH);
1554
+ img.srcset = srcset(src, PICKER_WIDTHS);
1555
+ };
1429
1556
  const firstAvailable = p.variants.find((v) => v.available) ?? null;
1430
- const initialThumbSrc = firstAvailable && firstAvailable.image || p.featuredImage;
1431
- if (initialThumbSrc) {
1432
- const img = document.createElement("img");
1433
- img.src = imgWidth(initialThumbSrc, PICKER_BASE_WIDTH);
1434
- img.srcset = srcset(initialThumbSrc, PICKER_WIDTHS);
1435
- img.sizes = PICKER_SIZES;
1436
- img.alt = p.title;
1437
- img.loading = "lazy";
1438
- thumbDiv.appendChild(img);
1439
- }
1557
+ paintThumb(firstAvailable && firstAvailable.image || p.featuredImage);
1440
1558
  row.appendChild(thumbDiv);
1441
1559
  const infoDiv = document.createElement("div");
1442
1560
  infoDiv.className = "lb-mix-match__modal-product-info";
@@ -1647,12 +1765,7 @@ function buildPickerRow(p, rowIndex, ctx) {
1647
1765
  unitPriceEl.textContent = "";
1648
1766
  unitPriceEl.hidden = true;
1649
1767
  }
1650
- const rowImg = thumbDiv.querySelector("img");
1651
- const imgSrc = v.image || p.featuredImage;
1652
- if (rowImg && imgSrc) {
1653
- rowImg.src = imgWidth(imgSrc, PICKER_BASE_WIDTH);
1654
- rowImg.srcset = srcset(imgSrc, PICKER_WIDTHS);
1655
- }
1768
+ paintThumb(v.image || p.featuredImage);
1656
1769
  setBoundsForVariant?.(v, capacity.variantUnitsElsewhere(v.id));
1657
1770
  if (v.options) recomputeDisabled(v.options);
1658
1771
  refresh();
@@ -2230,6 +2343,8 @@ function buildSlotCard(item, trans, formatMoney, removable, onRemove, onEdit) {
2230
2343
  img.alt = item.title;
2231
2344
  img.loading = "lazy";
2232
2345
  thumb.appendChild(img);
2346
+ } else {
2347
+ showThumbnailPlaceholder(thumb);
2233
2348
  }
2234
2349
  card.appendChild(thumb);
2235
2350
  const info = document.createElement("div");
@@ -4546,6 +4661,15 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4546
4661
  display: block;
4547
4662
  }
4548
4663
 
4664
+ /* Same reset, one more victim. A thumbnail is emitted empty and filled at
4665
+ hydration, so between the two it matched \`div:empty\` and vanished \u2014 taking
4666
+ the row's gap with it and starting that title 72px left of every other
4667
+ row's (issue #552). The box holds its place either way now: the product's
4668
+ image lands in it, or the placeholder mark does. */
4669
+ .lb-bundle-thumbnail:empty {
4670
+ display: flex;
4671
+ }
4672
+
4549
4673
  /* Divider */
4550
4674
  .lb-bundle-divider {
4551
4675
  height: var(--lb-border-width);
@@ -4584,11 +4708,6 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4584
4708
  object-fit: var(--lb-thumbnail-img-fit, cover);
4585
4709
  }
4586
4710
 
4587
- .lb-bundle-thumbnail svg {
4588
- width: 28px;
4589
- height: 28px;
4590
- color: color-mix(in srgb, var(--lb-text) 35%, transparent);
4591
- }
4592
4711
 
4593
4712
  .lb-bundle-product-info {
4594
4713
  flex: 1;
@@ -5030,15 +5149,33 @@ a.lb-bundle-product-name:focus-visible {
5030
5149
  border: 0 !important;
5031
5150
  }
5032
5151
 
5033
- /* Placeholder SVG icon for missing images */
5152
+ /* Missing-image placeholder.
5153
+ Two marks share these rules: Shopify's own \`placeholder_svg_tag\`
5154
+ illustration, which the theme scripts clone out of the <template> in
5155
+ bundle-widget.liquid, and the line glyph the headless renderer falls back to
5156
+ when there is no Liquid to supply one. */
5157
+ .lb-thumb--placeholder > [data-lb-placeholder] {
5158
+ display: block;
5159
+ width: 100%;
5160
+ /* The mark carries the box's height, not the other way round: the
5161
+ "original" thumbnail ratio sizes a box to its image's proportions and an
5162
+ imageless row has no image, so without this the box would be 60px by
5163
+ nothing. A box that already has a ratio caps the mark instead. */
5164
+ height: auto;
5165
+ max-height: 100%;
5166
+ aspect-ratio: 1 / 1;
5167
+ }
5168
+
5034
5169
  .lb-bundle-placeholder-icon {
5035
- width: 28px;
5036
- height: 28px;
5037
5170
  stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);
5038
5171
  stroke-width: 1.5;
5039
5172
  fill: none;
5040
5173
  }
5041
5174
 
5175
+ .lb-bundle-placeholder-svg {
5176
+ fill: color-mix(in srgb, var(--lb-text) 22%, transparent);
5177
+ }
5178
+
5042
5179
  /* Out-of-stock product row */
5043
5180
  .lb-bundle-product-row--oos {
5044
5181
  opacity: 0.5;
@@ -7580,7 +7717,33 @@ var LimeBundleElement = class extends HTMLElement {
7580
7717
  const key = cartStorageKey(this.shopDomain);
7581
7718
  let cartId = storage?.getItem(key) ?? null;
7582
7719
  const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
7583
- const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
7720
+ const requested = lines.map((line) => ({
7721
+ variantId: line.merchandiseId,
7722
+ quantity: line.quantity
7723
+ }));
7724
+ const bundleLinesOf = (payload) => {
7725
+ const nodes = payload?.cart?.lines?.nodes;
7726
+ if (!nodes) return null;
7727
+ const found = [];
7728
+ for (const node of nodes) {
7729
+ if (!node.attributes.some((a) => a.key === BUNDLE_GID_ATTRIBUTE)) {
7730
+ continue;
7731
+ }
7732
+ const variantId = node.merchandise?.id;
7733
+ if (!variantId) continue;
7734
+ found.push({ variantId, quantity: node.quantity });
7735
+ }
7736
+ return found;
7737
+ };
7738
+ const fellShort = (payload) => {
7739
+ const after = bundleLinesOf(payload);
7740
+ if (!after) return hasStockWarning(payload?.warnings);
7741
+ return bundleAddFellShort(requested, [], after);
7742
+ };
7743
+ const stockError = (payload) => formatCartStockError(
7744
+ DEFAULT_STRINGS.cartStockError,
7745
+ stockWarningMessage(payload?.warnings)
7746
+ );
7584
7747
  const fail = (message) => {
7585
7748
  this.dispatchEvent(
7586
7749
  new CustomEvent("lime-bundle:error", {
@@ -7611,8 +7774,8 @@ var LimeBundleElement = class extends HTMLElement {
7611
7774
  return;
7612
7775
  }
7613
7776
  storage?.removeItem(key);
7614
- } else if (stockCapped(payload?.warnings)) {
7615
- fail("Some items in this bundle are out of stock.");
7777
+ } else if (fellShort(payload)) {
7778
+ fail(stockError(payload));
7616
7779
  return;
7617
7780
  } else if (payload?.cart) {
7618
7781
  checkoutUrl = payload.cart.checkoutUrl;
@@ -7628,8 +7791,8 @@ var LimeBundleElement = class extends HTMLElement {
7628
7791
  fail(payload.userErrors[0].message);
7629
7792
  return;
7630
7793
  }
7631
- if (stockCapped(payload?.warnings)) {
7632
- fail("Some items in this bundle are out of stock.");
7794
+ if (fellShort(payload)) {
7795
+ fail(stockError(payload));
7633
7796
  return;
7634
7797
  }
7635
7798
  if (payload?.cart) {