@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.cjs CHANGED
@@ -52,6 +52,121 @@ function bundleLineAttributes(bundleGid, bundleType) {
52
52
  ];
53
53
  }
54
54
 
55
+ // ../render/src/cart.ts
56
+ var CART_STOCK_WARNING_CODES = [
57
+ "MERCHANDISE_NOT_ENOUGH_STOCK",
58
+ "MERCHANDISE_OUT_OF_STOCK"
59
+ ];
60
+ function isStockWarning(warning) {
61
+ return !!warning.code && CART_STOCK_WARNING_CODES.indexOf(warning.code) !== -1;
62
+ }
63
+ function hasStockWarning(warnings) {
64
+ const list = warnings || [];
65
+ for (let i = 0; i < list.length; i++) {
66
+ if (isStockWarning(list[i])) return true;
67
+ }
68
+ return false;
69
+ }
70
+ function stockWarningMessage(warnings) {
71
+ const list = warnings || [];
72
+ for (let i = 0; i < list.length; i++) {
73
+ const warning = list[i];
74
+ if (isStockWarning(warning) && warning.message) return warning.message;
75
+ }
76
+ return void 0;
77
+ }
78
+ function totalPerVariant(lines) {
79
+ const totals = /* @__PURE__ */ Object.create(null);
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i];
82
+ totals[line.variantId] = (totals[line.variantId] || 0) + (line.quantity || 0);
83
+ }
84
+ return totals;
85
+ }
86
+ function bundleAddFellShort(requested, before, after) {
87
+ const wanted = totalPerVariant(requested);
88
+ const had = totalPerVariant(before);
89
+ const has = totalPerVariant(after);
90
+ for (const variantId in wanted) {
91
+ if ((has[variantId] || 0) - (had[variantId] || 0) < wanted[variantId]) {
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+
98
+ // ../render/src/strings.ts
99
+ var DEFAULT_STRINGS = {
100
+ locale: "en",
101
+ addItem: "Add",
102
+ addedItem: "Added",
103
+ removeItem: "Remove",
104
+ swapItem: "Swap",
105
+ required: "Required",
106
+ soldOut: "Sold out",
107
+ outOfStock: "Out of stock",
108
+ itemsOutOfStock: {
109
+ one: "__COUNT__ item out of stock",
110
+ other: "__COUNT__ items out of stock"
111
+ },
112
+ cartStockError: "We couldn't add this bundle to your cart. __MESSAGE__",
113
+ quantity: "Quantity",
114
+ increaseQuantity: "Increase quantity",
115
+ decreaseQuantity: "Decrease quantity",
116
+ allTypes: "All",
117
+ editSelection: "Edit selection",
118
+ addToCart: "Add to cart",
119
+ bundlePrice: "Bundle total",
120
+ save: "Save",
121
+ complete: "Complete",
122
+ ofSelected: "__COUNT__ of __TOTAL__ added",
123
+ moreToGo: "__COUNT__ more to go",
124
+ nProductsShown: "__COUNT__ products shown",
125
+ bundleComplete: "Your bundle is complete",
126
+ chooseMoreUnlock: "Choose __COUNT__ more to unlock __PCT__% off",
127
+ chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
128
+ stepOf: "Step __STEP__ of __TOTAL__",
129
+ stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
130
+ doorChoose: "Choose your products",
131
+ doorContinue: "Continue building",
132
+ doorQuickSteps: {
133
+ one: "__COUNT__ quick step",
134
+ other: "__COUNT__ quick steps"
135
+ },
136
+ doorStepsToGo: {
137
+ one: "__COUNT__ step to go",
138
+ other: "__COUNT__ steps to go"
139
+ },
140
+ each: " each",
141
+ buyQty: "Buy __COUNT__",
142
+ itemCount: {
143
+ one: "__COUNT__ item",
144
+ other: "__COUNT__ items"
145
+ },
146
+ needsSpots: {
147
+ one: "Needs __COUNT__ spot",
148
+ other: "Needs __COUNT__ spots"
149
+ }
150
+ };
151
+ function formatItemsOutOfStock(t, count) {
152
+ const raw = t.itemsOutOfStock;
153
+ let tpl;
154
+ if (raw && typeof raw === "object") {
155
+ const form = new Intl.PluralRules(t.locale || "en").select(count);
156
+ tpl = raw[form] || raw.other;
157
+ } else {
158
+ tpl = raw;
159
+ }
160
+ return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
161
+ }
162
+ var CART_STOCK_ERROR_FALLBACK = "We couldn't add this bundle to your cart. __MESSAGE__";
163
+ function formatCartStockError(template, message) {
164
+ const tpl = (template || "").trim() || CART_STOCK_ERROR_FALLBACK;
165
+ const warning = (message || "").trim();
166
+ const composed = tpl.indexOf("__MESSAGE__") === -1 ? tpl + " " + warning : tpl.split("__MESSAGE__").join(warning);
167
+ return composed.replace(/\s+/g, " ").trim();
168
+ }
169
+
55
170
  // ../render/src/adapt.ts
56
171
  var import_core = require("@lime-bundles/core");
57
172
  function numericId(gid) {
@@ -381,6 +496,55 @@ function buildOptionGroups(container, product, selected) {
381
496
  }
382
497
  }
383
498
 
499
+ // ../render/src/product/placeholder.ts
500
+ var SVG_NS = "http://www.w3.org/2000/svg";
501
+ var PLACEHOLDER_ATTR = "data-lb-placeholder";
502
+ var PLACEHOLDER_CLASS = "lb-thumb--placeholder";
503
+ function builtInMark() {
504
+ const svg = document.createElementNS(SVG_NS, "svg");
505
+ svg.setAttribute("class", "lb-bundle-placeholder-icon");
506
+ svg.setAttribute("viewBox", "0 0 48 48");
507
+ const rect = document.createElementNS(SVG_NS, "rect");
508
+ rect.setAttribute("x", "15");
509
+ rect.setAttribute("y", "15");
510
+ rect.setAttribute("width", "18");
511
+ rect.setAttribute("height", "18");
512
+ rect.setAttribute("rx", "2");
513
+ svg.appendChild(rect);
514
+ const dot = document.createElementNS(SVG_NS, "circle");
515
+ dot.setAttribute("cx", "20.5");
516
+ dot.setAttribute("cy", "20.5");
517
+ dot.setAttribute("r", "1.5");
518
+ svg.appendChild(dot);
519
+ const ridge = document.createElementNS(SVG_NS, "path");
520
+ ridge.setAttribute("d", "M33 27l-5-5L17 33");
521
+ svg.appendChild(ridge);
522
+ return svg;
523
+ }
524
+ var make = builtInMark;
525
+ function mark() {
526
+ const node = make();
527
+ const el2 = node.nodeType === 1 ? node : null;
528
+ if (el2) {
529
+ el2.setAttribute(PLACEHOLDER_ATTR, "");
530
+ el2.setAttribute("aria-hidden", "true");
531
+ el2.setAttribute("focusable", "false");
532
+ }
533
+ return node;
534
+ }
535
+ function showThumbnailPlaceholder(box2) {
536
+ const img = box2.querySelector("img");
537
+ if (img) img.remove();
538
+ box2.classList.add(PLACEHOLDER_CLASS);
539
+ if (box2.querySelector("[" + PLACEHOLDER_ATTR + "]")) return;
540
+ box2.appendChild(mark());
541
+ }
542
+ function hideThumbnailPlaceholder(box2) {
543
+ const existing = box2.querySelector("[" + PLACEHOLDER_ATTR + "]");
544
+ if (existing) existing.remove();
545
+ box2.classList.remove(PLACEHOLDER_CLASS);
546
+ }
547
+
384
548
  // ../render/src/product/row.ts
385
549
  var productImgSrc = /* @__PURE__ */ new WeakMap();
386
550
  function updateThumbnail(row, variant, productImage) {
@@ -391,8 +555,12 @@ function updateThumbnail(row, variant, productImage) {
391
555
  const seed = productImage || (img ? img.src : null);
392
556
  if (seed) productImgSrc.set(row, seed);
393
557
  }
394
- const nextSrc = variant.image || productImgSrc.get(row) || null;
395
- if (!nextSrc) return;
558
+ const nextSrc = variant && variant.image || productImgSrc.get(row) || null;
559
+ if (!nextSrc) {
560
+ showThumbnailPlaceholder(thumb);
561
+ return;
562
+ }
563
+ hideThumbnailPlaceholder(thumb);
396
564
  if (img) {
397
565
  img.src = nextSrc;
398
566
  img.srcset = "";
@@ -411,7 +579,10 @@ function updateInlineQuantity(row, qty) {
411
579
  el2.textContent = "\xD7" + qty;
412
580
  }
413
581
  function applyVariant(row, variant, product, formatMoney) {
414
- if (!variant) return;
582
+ if (!variant) {
583
+ updateThumbnail(row, null, product.featuredImage);
584
+ return;
585
+ }
415
586
  updateThumbnail(row, variant, product.featuredImage);
416
587
  updateInlineQuantity(row, variant.quantity || product.quantity || 1);
417
588
  const priceEl = row.querySelector("[data-product-price]");
@@ -461,8 +632,11 @@ function buildRowSkeleton(product, t, opts) {
461
632
  if (opts.bogoRole) row.setAttribute("data-bogo-role", opts.bogoRole);
462
633
  const thumb = box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
463
634
  const ratio = product.featuredImageRatio;
464
- if (opts.thumbnailRatio === "original" && ratio != null && ratio > 0) {
465
- thumb.style.setProperty("--lb-row-thumb-ratio", String(ratio));
635
+ if (opts.thumbnailRatio === "original") {
636
+ thumb.style.setProperty(
637
+ "--lb-row-thumb-ratio",
638
+ String(ratio != null && ratio > 0 ? ratio : 1)
639
+ );
466
640
  }
467
641
  row.appendChild(thumb);
468
642
  const info = box("div", "lb-bundle-product-info");
@@ -744,70 +918,6 @@ function initCountdown(container) {
744
918
  }
745
919
  }
746
920
 
747
- // ../render/src/strings.ts
748
- var DEFAULT_STRINGS = {
749
- locale: "en",
750
- addItem: "Add",
751
- addedItem: "Added",
752
- removeItem: "Remove",
753
- swapItem: "Swap",
754
- required: "Required",
755
- soldOut: "Sold out",
756
- outOfStock: "Out of stock",
757
- itemsOutOfStock: {
758
- one: "__COUNT__ item out of stock",
759
- other: "__COUNT__ items out of stock"
760
- },
761
- quantity: "Quantity",
762
- increaseQuantity: "Increase quantity",
763
- decreaseQuantity: "Decrease quantity",
764
- allTypes: "All",
765
- editSelection: "Edit selection",
766
- addToCart: "Add to cart",
767
- bundlePrice: "Bundle total",
768
- save: "Save",
769
- complete: "Complete",
770
- ofSelected: "__COUNT__ of __TOTAL__ added",
771
- moreToGo: "__COUNT__ more to go",
772
- nProductsShown: "__COUNT__ products shown",
773
- bundleComplete: "Your bundle is complete",
774
- chooseMoreUnlock: "Choose __COUNT__ more to unlock __PCT__% off",
775
- chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
776
- stepOf: "Step __STEP__ of __TOTAL__",
777
- stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
778
- doorChoose: "Choose your products",
779
- doorContinue: "Continue building",
780
- doorQuickSteps: {
781
- one: "__COUNT__ quick step",
782
- other: "__COUNT__ quick steps"
783
- },
784
- doorStepsToGo: {
785
- one: "__COUNT__ step to go",
786
- other: "__COUNT__ steps to go"
787
- },
788
- each: " each",
789
- buyQty: "Buy __COUNT__",
790
- itemCount: {
791
- one: "__COUNT__ item",
792
- other: "__COUNT__ items"
793
- },
794
- needsSpots: {
795
- one: "Needs __COUNT__ spot",
796
- other: "Needs __COUNT__ spots"
797
- }
798
- };
799
- function formatItemsOutOfStock(t, count) {
800
- const raw = t.itemsOutOfStock;
801
- let tpl;
802
- if (raw && typeof raw === "object") {
803
- const form = new Intl.PluralRules(t.locale || "en").select(count);
804
- tpl = raw[form] || raw.other;
805
- } else {
806
- tpl = raw;
807
- }
808
- return (tpl || "__COUNT__ items out of stock").split("__COUNT__").join(String(count));
809
- }
810
-
811
921
  // ../render/src/dropdown/bind.ts
812
922
  var import_core2 = require("@lime-bundles/core");
813
923
 
@@ -1427,17 +1537,25 @@ function buildPickerRow(p, rowIndex, ctx) {
1427
1537
  addedBadge.textContent = t.addedItem || "Added";
1428
1538
  addedBadge.hidden = true;
1429
1539
  thumbDiv.appendChild(addedBadge);
1540
+ const paintThumb = (src) => {
1541
+ if (!src) {
1542
+ showThumbnailPlaceholder(thumbDiv);
1543
+ return;
1544
+ }
1545
+ hideThumbnailPlaceholder(thumbDiv);
1546
+ let img = thumbDiv.querySelector("img");
1547
+ if (!img) {
1548
+ img = document.createElement("img");
1549
+ img.sizes = PICKER_SIZES;
1550
+ img.alt = p.title;
1551
+ img.loading = "lazy";
1552
+ thumbDiv.appendChild(img);
1553
+ }
1554
+ img.src = imgWidth(src, PICKER_BASE_WIDTH);
1555
+ img.srcset = srcset(src, PICKER_WIDTHS);
1556
+ };
1430
1557
  const firstAvailable = p.variants.find((v) => v.available) ?? null;
1431
- const initialThumbSrc = firstAvailable && firstAvailable.image || p.featuredImage;
1432
- if (initialThumbSrc) {
1433
- const img = document.createElement("img");
1434
- img.src = imgWidth(initialThumbSrc, PICKER_BASE_WIDTH);
1435
- img.srcset = srcset(initialThumbSrc, PICKER_WIDTHS);
1436
- img.sizes = PICKER_SIZES;
1437
- img.alt = p.title;
1438
- img.loading = "lazy";
1439
- thumbDiv.appendChild(img);
1440
- }
1558
+ paintThumb(firstAvailable && firstAvailable.image || p.featuredImage);
1441
1559
  row.appendChild(thumbDiv);
1442
1560
  const infoDiv = document.createElement("div");
1443
1561
  infoDiv.className = "lb-mix-match__modal-product-info";
@@ -1648,12 +1766,7 @@ function buildPickerRow(p, rowIndex, ctx) {
1648
1766
  unitPriceEl.textContent = "";
1649
1767
  unitPriceEl.hidden = true;
1650
1768
  }
1651
- const rowImg = thumbDiv.querySelector("img");
1652
- const imgSrc = v.image || p.featuredImage;
1653
- if (rowImg && imgSrc) {
1654
- rowImg.src = imgWidth(imgSrc, PICKER_BASE_WIDTH);
1655
- rowImg.srcset = srcset(imgSrc, PICKER_WIDTHS);
1656
- }
1769
+ paintThumb(v.image || p.featuredImage);
1657
1770
  setBoundsForVariant?.(v, capacity.variantUnitsElsewhere(v.id));
1658
1771
  if (v.options) recomputeDisabled(v.options);
1659
1772
  refresh();
@@ -2231,6 +2344,8 @@ function buildSlotCard(item, trans, formatMoney, removable, onRemove, onEdit) {
2231
2344
  img.alt = item.title;
2232
2345
  img.loading = "lazy";
2233
2346
  thumb.appendChild(img);
2347
+ } else {
2348
+ showThumbnailPlaceholder(thumb);
2234
2349
  }
2235
2350
  card.appendChild(thumb);
2236
2351
  const info = document.createElement("div");
@@ -4536,6 +4651,15 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4536
4651
  display: block;
4537
4652
  }
4538
4653
 
4654
+ /* Same reset, one more victim. A thumbnail is emitted empty and filled at
4655
+ hydration, so between the two it matched \`div:empty\` and vanished \u2014 taking
4656
+ the row's gap with it and starting that title 72px left of every other
4657
+ row's (issue #552). The box holds its place either way now: the product's
4658
+ image lands in it, or the placeholder mark does. */
4659
+ .lb-bundle-thumbnail:empty {
4660
+ display: flex;
4661
+ }
4662
+
4539
4663
  /* Divider */
4540
4664
  .lb-bundle-divider {
4541
4665
  height: var(--lb-border-width);
@@ -4574,11 +4698,6 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4574
4698
  object-fit: var(--lb-thumbnail-img-fit, cover);
4575
4699
  }
4576
4700
 
4577
- .lb-bundle-thumbnail svg {
4578
- width: 28px;
4579
- height: 28px;
4580
- color: color-mix(in srgb, var(--lb-text) 35%, transparent);
4581
- }
4582
4701
 
4583
4702
  .lb-bundle-product-info {
4584
4703
  flex: 1;
@@ -5020,15 +5139,33 @@ a.lb-bundle-product-name:focus-visible {
5020
5139
  border: 0 !important;
5021
5140
  }
5022
5141
 
5023
- /* Placeholder SVG icon for missing images */
5142
+ /* Missing-image placeholder.
5143
+ Two marks share these rules: Shopify's own \`placeholder_svg_tag\`
5144
+ illustration, which the theme scripts clone out of the <template> in
5145
+ bundle-widget.liquid, and the line glyph the headless renderer falls back to
5146
+ when there is no Liquid to supply one. */
5147
+ .lb-thumb--placeholder > [data-lb-placeholder] {
5148
+ display: block;
5149
+ width: 100%;
5150
+ /* The mark carries the box's height, not the other way round: the
5151
+ "original" thumbnail ratio sizes a box to its image's proportions and an
5152
+ imageless row has no image, so without this the box would be 60px by
5153
+ nothing. A box that already has a ratio caps the mark instead. */
5154
+ height: auto;
5155
+ max-height: 100%;
5156
+ aspect-ratio: 1 / 1;
5157
+ }
5158
+
5024
5159
  .lb-bundle-placeholder-icon {
5025
- width: 28px;
5026
- height: 28px;
5027
5160
  stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);
5028
5161
  stroke-width: 1.5;
5029
5162
  fill: none;
5030
5163
  }
5031
5164
 
5165
+ .lb-bundle-placeholder-svg {
5166
+ fill: color-mix(in srgb, var(--lb-text) 22%, transparent);
5167
+ }
5168
+
5032
5169
  /* Out-of-stock product row */
5033
5170
  .lb-bundle-product-row--oos {
5034
5171
  opacity: 0.5;
@@ -7570,7 +7707,33 @@ var LimeBundleElement = class extends HTMLElement {
7570
7707
  const key = cartStorageKey(this.shopDomain);
7571
7708
  let cartId = storage?.getItem(key) ?? null;
7572
7709
  const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
7573
- const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
7710
+ const requested = lines.map((line) => ({
7711
+ variantId: line.merchandiseId,
7712
+ quantity: line.quantity
7713
+ }));
7714
+ const bundleLinesOf = (payload) => {
7715
+ const nodes = payload?.cart?.lines?.nodes;
7716
+ if (!nodes) return null;
7717
+ const found = [];
7718
+ for (const node of nodes) {
7719
+ if (!node.attributes.some((a) => a.key === BUNDLE_GID_ATTRIBUTE)) {
7720
+ continue;
7721
+ }
7722
+ const variantId = node.merchandise?.id;
7723
+ if (!variantId) continue;
7724
+ found.push({ variantId, quantity: node.quantity });
7725
+ }
7726
+ return found;
7727
+ };
7728
+ const fellShort = (payload) => {
7729
+ const after = bundleLinesOf(payload);
7730
+ if (!after) return hasStockWarning(payload?.warnings);
7731
+ return bundleAddFellShort(requested, [], after);
7732
+ };
7733
+ const stockError = (payload) => formatCartStockError(
7734
+ DEFAULT_STRINGS.cartStockError,
7735
+ stockWarningMessage(payload?.warnings)
7736
+ );
7574
7737
  const fail = (message) => {
7575
7738
  this.dispatchEvent(
7576
7739
  new CustomEvent("lime-bundle:error", {
@@ -7601,8 +7764,8 @@ var LimeBundleElement = class extends HTMLElement {
7601
7764
  return;
7602
7765
  }
7603
7766
  storage?.removeItem(key);
7604
- } else if (stockCapped(payload?.warnings)) {
7605
- fail("Some items in this bundle are out of stock.");
7767
+ } else if (fellShort(payload)) {
7768
+ fail(stockError(payload));
7606
7769
  return;
7607
7770
  } else if (payload?.cart) {
7608
7771
  checkoutUrl = payload.cart.checkoutUrl;
@@ -7618,8 +7781,8 @@ var LimeBundleElement = class extends HTMLElement {
7618
7781
  fail(payload.userErrors[0].message);
7619
7782
  return;
7620
7783
  }
7621
- if (stockCapped(payload?.warnings)) {
7622
- fail("Some items in this bundle are out of stock.");
7784
+ if (fellShort(payload)) {
7785
+ fail(stockError(payload));
7623
7786
  return;
7624
7787
  }
7625
7788
  if (payload?.cart) {