@lime-bundles/widget 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  BUNDLES_FOR_PRODUCT_QUERY,
6
6
  CART_CREATE_MUTATION,
7
7
  CART_LINES_ADD_MUTATION,
8
+ CART_LINES_QUERY,
9
+ CART_LINES_REMOVE_MUTATION,
8
10
  SHOP_SETTINGS_QUERY,
9
11
  parseMetaobjectBundle,
10
12
  observeImpression,
@@ -19,6 +21,28 @@ import {
19
21
  resolveAbTests
20
22
  } from "@lime-bundles/core";
21
23
 
24
+ // ../render/src/host.ts
25
+ function intlFormatMoney(currencyCode) {
26
+ return (cents) => {
27
+ try {
28
+ return new Intl.NumberFormat(void 0, {
29
+ style: "currency",
30
+ currency: currencyCode
31
+ }).format(cents / 100);
32
+ } catch {
33
+ return currencyCode + " " + (cents / 100).toFixed(2);
34
+ }
35
+ };
36
+ }
37
+ var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
38
+ var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
39
+ function bundleLineAttributes(bundleGid, bundleType) {
40
+ return [
41
+ { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
42
+ { key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
43
+ ];
44
+ }
45
+
22
46
  // ../render/src/adapt.ts
23
47
  import {
24
48
  formatUnitPrice,
@@ -36,6 +60,14 @@ function toCents(amount) {
36
60
  const n = parseFloat(amount);
37
61
  return Number.isFinite(n) ? Math.round(n * 100) : 0;
38
62
  }
63
+ function imageAspectRatio(image) {
64
+ const w = image?.width;
65
+ const h = image?.height;
66
+ if (typeof w !== "number" || typeof h !== "number" || w <= 0 || h <= 0) {
67
+ return null;
68
+ }
69
+ return w / h;
70
+ }
39
71
  function adaptVariant(v, requiredQty, fallbackCurrency) {
40
72
  return {
41
73
  id: numericId(v.id),
@@ -77,6 +109,7 @@ function adaptProduct(product, bundleId, opts) {
77
109
  title: product.title,
78
110
  url: product.handle ? "/products/" + product.handle : null,
79
111
  featuredImage: product.featuredImage?.url ?? null,
112
+ featuredImageRatio: imageAspectRatio(product.featuredImage),
80
113
  quantity: productQty,
81
114
  selectedVariantId: initial ? initial.id : "",
82
115
  // Option names come off a variant, since the Storefront product query
@@ -226,28 +259,6 @@ function recalcPricing(container, products, formatMoney) {
226
259
  }
227
260
  }
228
261
 
229
- // ../render/src/host.ts
230
- function intlFormatMoney(currencyCode) {
231
- return (cents) => {
232
- try {
233
- return new Intl.NumberFormat(void 0, {
234
- style: "currency",
235
- currency: currencyCode
236
- }).format(cents / 100);
237
- } catch {
238
- return currencyCode + " " + (cents / 100).toFixed(2);
239
- }
240
- };
241
- }
242
- var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
243
- var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
244
- function bundleLineAttributes(bundleGid, bundleType) {
245
- return [
246
- { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
247
- { key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
248
- ];
249
- }
250
-
251
262
  // ../render/src/product/option-selects.ts
252
263
  function bindVariantSelects(row, getProduct, onSelected) {
253
264
  const optionSelects = row.querySelectorAll("[data-variant-option]");
@@ -443,7 +454,12 @@ function buildRowSkeleton(product, t, opts) {
443
454
  }
444
455
  row.setAttribute("data-product-id", String(product.productId));
445
456
  if (opts.bogoRole) row.setAttribute("data-bogo-role", opts.bogoRole);
446
- row.appendChild(box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" }));
457
+ const thumb = box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
458
+ const ratio = product.featuredImageRatio;
459
+ if (opts.thumbnailRatio === "original" && ratio != null && ratio > 0) {
460
+ thumb.style.setProperty("--lb-row-thumb-ratio", String(ratio));
461
+ }
462
+ row.appendChild(thumb);
447
463
  const info = box("div", "lb-bundle-product-info");
448
464
  const title = document.createElement(opts.href ? "a" : "span");
449
465
  title.className = "lb-bundle-product-name";
@@ -752,6 +768,16 @@ var DEFAULT_STRINGS = {
752
768
  chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
753
769
  stepOf: "Step __STEP__ of __TOTAL__",
754
770
  stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
771
+ doorChoose: "Choose your products",
772
+ doorContinue: "Continue building",
773
+ doorQuickSteps: {
774
+ one: "__COUNT__ quick step",
775
+ other: "__COUNT__ quick steps"
776
+ },
777
+ doorStepsToGo: {
778
+ one: "__COUNT__ step to go",
779
+ other: "__COUNT__ steps to go"
780
+ },
755
781
  each: " each",
756
782
  buyQty: "Buy __COUNT__",
757
783
  itemCount: {
@@ -1209,7 +1235,8 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
1209
1235
  const isOos = !product.variants.some((v) => v.available);
1210
1236
  const row = buildRowSkeleton(product, t, {
1211
1237
  isOos,
1212
- href: product.url
1238
+ href: product.url,
1239
+ thumbnailRatio: wc.productList?.thumbnailRatio
1213
1240
  });
1214
1241
  shell.products.appendChild(row);
1215
1242
  if (isOos) continue;
@@ -2809,6 +2836,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, current
2809
2836
  import {
2810
2837
  bestValueTierIndex,
2811
2838
  isVariantFulfillable as isVariantFulfillable2,
2839
+ resolveContextProduct,
2812
2840
  resolveDefaultTierIndex,
2813
2841
  resolvePopularTierIndex,
2814
2842
  stockCapForVariant as stockCapForVariant2
@@ -2941,9 +2969,9 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
2941
2969
  }
2942
2970
 
2943
2971
  // src/renderers/volume.ts
2944
- function renderVolumeBundle(container, bundle, onAddToCart) {
2972
+ function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
2945
2973
  const wc = bundle.widgetConfig;
2946
- const product = bundle.products[0];
2974
+ const product = resolveContextProduct(bundle.products, productContext);
2947
2975
  if (!product) return;
2948
2976
  const variants = product.variants.nodes;
2949
2977
  const minTierQty = Math.min(
@@ -3214,7 +3242,8 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3214
3242
  href: side.url,
3215
3243
  bogoRole: side.role,
3216
3244
  inlineQuantity: isGet ? getQty : buyQty,
3217
- badgeText: isGet && !isOos ? badgeText : null
3245
+ badgeText: isGet && !isOos ? badgeText : null,
3246
+ thumbnailRatio: wc.productList?.thumbnailRatio
3218
3247
  });
3219
3248
  shell.products.appendChild(row);
3220
3249
  if (isOos) return;
@@ -3283,6 +3312,32 @@ function stepsCompletedLabel(completed, total, t) {
3283
3312
  function moreToGoLabel(remaining, t) {
3284
3313
  return remaining > 0 ? fill(t.moreToGo || "__COUNT__ more to go", { COUNT: remaining }) : t.complete || "Complete";
3285
3314
  }
3315
+ function fillPlural(map, count, locale) {
3316
+ const form = new Intl.PluralRules(locale || "en").select(count);
3317
+ return fill(map[form] || map.other || "__COUNT__", { COUNT: count });
3318
+ }
3319
+ function doorLabel(anyPicks, t) {
3320
+ return anyPicks ? t.doorContinue || "Continue building" : t.doorChoose || "Choose your products";
3321
+ }
3322
+ function doorSubLabel(anyPicks, remaining, total, t) {
3323
+ if (!anyPicks) {
3324
+ const quick = t.doorQuickSteps;
3325
+ if (typeof quick === "string") return fill(quick, { COUNT: total });
3326
+ return fillPlural(
3327
+ quick || { one: "__COUNT__ quick step", other: "__COUNT__ quick steps" },
3328
+ total,
3329
+ t.locale
3330
+ );
3331
+ }
3332
+ return fillPlural(
3333
+ t.doorStepsToGo || {
3334
+ one: "__COUNT__ step to go",
3335
+ other: "__COUNT__ steps to go"
3336
+ },
3337
+ remaining,
3338
+ t.locale
3339
+ );
3340
+ }
3286
3341
 
3287
3342
  // ../render/src/multi-step/rules.ts
3288
3343
  function unitsInStep(selections, i) {
@@ -3344,20 +3399,28 @@ function updateStepGroups(deps) {
3344
3399
  const { container, steps, selections, t } = deps;
3345
3400
  for (let i = 0; i < steps.length; i++) {
3346
3401
  const min = steps[i].minQuantity || 1;
3347
- const countEl = container.querySelector(
3348
- '[data-step-count="' + i + '"]'
3402
+ const section = container.querySelector(
3403
+ '[data-step-group="' + i + '"]'
3349
3404
  );
3350
- if (countEl) {
3351
- countEl.textContent = ofSelectedLabel(
3352
- Math.min(unitsInStep(selections, i), min),
3353
- min,
3354
- t
3355
- );
3356
- countEl.classList.toggle(
3357
- "lb-multi-step__group-count--met",
3358
- stepSatisfied(selections, steps, i)
3405
+ if (section) {
3406
+ section.setAttribute(
3407
+ "data-step-mode",
3408
+ selections[i].length > 0 ? "open" : "row"
3359
3409
  );
3360
3410
  }
3411
+ const countLabel = ofSelectedLabel(
3412
+ Math.min(unitsInStep(selections, i), min),
3413
+ min,
3414
+ t
3415
+ );
3416
+ const met = stepSatisfied(selections, steps, i);
3417
+ const countEls = container.querySelectorAll(
3418
+ '[data-step-count="' + i + '"]'
3419
+ );
3420
+ for (let c = 0; c < countEls.length; c++) {
3421
+ countEls[c].textContent = countLabel;
3422
+ countEls[c].classList.toggle("lb-multi-step__group-count--met", met);
3423
+ }
3361
3424
  const slotsWrap = container.querySelector(
3362
3425
  '[data-step-slots="' + i + '"]'
3363
3426
  );
@@ -3379,14 +3442,50 @@ function updateStepGroups(deps) {
3379
3442
  );
3380
3443
  }
3381
3444
  }
3382
- for (let ti = 0; ti < steps.length; ti++) {
3383
- const trigger = container.querySelector(
3384
- '[data-step-trigger="' + ti + '"]'
3385
- );
3386
- if (trigger) {
3387
- trigger.style.display = stepHeadroom(selections, steps, ti) === 0 ? "none" : "";
3445
+ }
3446
+ function updateDoor(deps) {
3447
+ const { container, steps, selections, t } = deps;
3448
+ const door = container.querySelector("[data-step-door]");
3449
+ if (!door) return;
3450
+ let firstUnsatisfied = -1;
3451
+ let remaining = 0;
3452
+ let anyPicks = false;
3453
+ for (let i = 0; i < steps.length; i++) {
3454
+ if (selections[i].length > 0) anyPicks = true;
3455
+ if (!stepSatisfied(selections, steps, i)) {
3456
+ remaining += 1;
3457
+ if (firstUnsatisfied === -1) firstUnsatisfied = i;
3388
3458
  }
3389
3459
  }
3460
+ if (firstUnsatisfied === -1) {
3461
+ door.style.display = "none";
3462
+ door.classList.add("lb-multi-step__door--hidden");
3463
+ const parent = door.parentElement;
3464
+ if (parent && parent.firstElementChild !== door) {
3465
+ parent.insertBefore(door, parent.firstElementChild);
3466
+ }
3467
+ return;
3468
+ }
3469
+ door.style.display = "";
3470
+ door.classList.remove("lb-multi-step__door--hidden");
3471
+ door.setAttribute("data-step-door", String(firstUnsatisfied));
3472
+ const label = doorLabel(anyPicks, t);
3473
+ const labelEl = door.querySelector(
3474
+ ".lb-mix-match__add-product-label"
3475
+ );
3476
+ if (labelEl) labelEl.textContent = label;
3477
+ const subEl = door.querySelector(".lb-multi-step__door-sub");
3478
+ if (subEl) {
3479
+ subEl.textContent = doorSubLabel(anyPicks, remaining, steps.length, t);
3480
+ }
3481
+ const stepName = steps[firstUnsatisfied].name || "";
3482
+ door.setAttribute("aria-label", stepName ? label + ": " + stepName : label);
3483
+ const target = container.querySelector(
3484
+ '[data-step-group="' + firstUnsatisfied + '"]'
3485
+ );
3486
+ if (target && target.parentElement && target.previousElementSibling !== door) {
3487
+ target.parentElement.insertBefore(door, target);
3488
+ }
3390
3489
  }
3391
3490
  function updatePricing3(deps, data, calcDiscount, paint) {
3392
3491
  const { container, steps, selections } = deps;
@@ -3830,10 +3929,12 @@ function createWizard(deps) {
3830
3929
  setTimeout(() => {
3831
3930
  if (!modalOpen) overlay.style.display = "none";
3832
3931
  }, CLOSE_TRANSITION_MS2);
3833
- const stepTrigger = container.querySelector(
3834
- '[data-step-trigger="' + stepIndex + '"]'
3835
- );
3836
- const elToFocus = stepTrigger && stepTrigger.offsetParent !== null ? stepTrigger : deps.ctaButton;
3932
+ const visible = (el2) => el2 && el2.offsetParent !== null ? el2 : null;
3933
+ const elToFocus = visible(
3934
+ container.querySelector(
3935
+ '[data-step-row="' + stepIndex + '"]'
3936
+ )
3937
+ ) || visible(container.querySelector("[data-step-door]")) || deps.ctaButton;
3837
3938
  if (elToFocus && typeof elToFocus.focus === "function") {
3838
3939
  setTimeout(() => elToFocus.focus(), 0);
3839
3940
  }
@@ -3886,6 +3987,29 @@ function createWizard(deps) {
3886
3987
  }
3887
3988
 
3888
3989
  // src/renderers/multi-step.ts
3990
+ function buildPlusIcon() {
3991
+ const NS = "http://www.w3.org/2000/svg";
3992
+ const svg = document.createElementNS(NS, "svg");
3993
+ svg.setAttribute("width", "18");
3994
+ svg.setAttribute("height", "18");
3995
+ svg.setAttribute("viewBox", "0 0 18 18");
3996
+ svg.setAttribute("fill", "none");
3997
+ for (const [x1, y1, x2, y2] of [
3998
+ ["9", "3", "9", "15"],
3999
+ ["3", "9", "15", "9"]
4000
+ ]) {
4001
+ const line = document.createElementNS(NS, "line");
4002
+ line.setAttribute("x1", x1);
4003
+ line.setAttribute("y1", y1);
4004
+ line.setAttribute("x2", x2);
4005
+ line.setAttribute("y2", y2);
4006
+ line.setAttribute("stroke", "currentColor");
4007
+ line.setAttribute("stroke-width", "2");
4008
+ line.setAttribute("stroke-linecap", "round");
4009
+ svg.appendChild(line);
4010
+ }
4011
+ return svg;
4012
+ }
3889
4013
  function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
3890
4014
  const wc = bundle.widgetConfig;
3891
4015
  const t = DEFAULT_STRINGS;
@@ -3940,31 +4064,65 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
3940
4064
  shell.products.before(buildProgressBar(steps.length));
3941
4065
  const groups = document.createElement("div");
3942
4066
  groups.className = "lb-multi-step__groups";
4067
+ groups.setAttribute("data-step-groups", "");
4068
+ const door = document.createElement("button");
4069
+ door.type = "button";
4070
+ door.className = "lb-mix-match__add-product lb-multi-step__door";
4071
+ door.setAttribute("data-step-door", "0");
4072
+ const doorIcon = document.createElement("span");
4073
+ doorIcon.className = "lb-mix-match__add-product-icon";
4074
+ doorIcon.setAttribute("aria-hidden", "true");
4075
+ doorIcon.appendChild(buildPlusIcon());
4076
+ door.appendChild(doorIcon);
4077
+ const doorCopy = document.createElement("span");
4078
+ doorCopy.className = "lb-multi-step__door-copy";
4079
+ const doorLabel2 = document.createElement("span");
4080
+ doorLabel2.className = "lb-mix-match__add-product-label";
4081
+ doorCopy.appendChild(doorLabel2);
4082
+ const doorSub = document.createElement("span");
4083
+ doorSub.className = "lb-multi-step__door-sub";
4084
+ doorCopy.appendChild(doorSub);
4085
+ door.appendChild(doorCopy);
4086
+ groups.appendChild(door);
3943
4087
  steps.forEach((step, i) => {
3944
- const group = document.createElement("div");
4088
+ const group = document.createElement("section");
3945
4089
  group.className = "lb-multi-step__group";
3946
4090
  group.setAttribute("data-step-group", String(i));
4091
+ group.setAttribute("data-step-mode", "row");
4092
+ const buildCount = () => {
4093
+ const count = document.createElement("span");
4094
+ count.className = "lb-multi-step__group-count";
4095
+ count.setAttribute("data-step-count", String(i));
4096
+ return count;
4097
+ };
4098
+ const row = document.createElement("button");
4099
+ row.type = "button";
4100
+ row.className = "lb-multi-step__step-row";
4101
+ row.setAttribute("data-step-row", String(i));
4102
+ row.setAttribute("aria-label", "Choose products: " + step.name);
4103
+ const ix = document.createElement("span");
4104
+ ix.className = "lb-multi-step__step-ix";
4105
+ ix.setAttribute("aria-hidden", "true");
4106
+ ix.textContent = String(i + 1);
4107
+ row.appendChild(ix);
4108
+ const rowName = document.createElement("span");
4109
+ rowName.className = "lb-multi-step__group-name";
4110
+ rowName.textContent = step.name;
4111
+ row.appendChild(rowName);
4112
+ row.appendChild(buildCount());
4113
+ group.appendChild(row);
3947
4114
  const heading = document.createElement("div");
3948
4115
  heading.className = "lb-multi-step__group-heading";
3949
4116
  const name = document.createElement("span");
3950
4117
  name.className = "lb-multi-step__group-name";
3951
4118
  name.textContent = step.name;
3952
4119
  heading.appendChild(name);
3953
- const count = document.createElement("span");
3954
- count.className = "lb-multi-step__group-count";
3955
- count.setAttribute("data-step-count", String(i));
3956
- heading.appendChild(count);
4120
+ heading.appendChild(buildCount());
3957
4121
  group.appendChild(heading);
3958
4122
  const slots = document.createElement("div");
3959
- slots.className = "lb-mix-match__slots lb-edge-fade";
4123
+ slots.className = "lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots";
3960
4124
  slots.setAttribute("data-step-slots", String(i));
3961
4125
  group.appendChild(slots);
3962
- const trigger = document.createElement("button");
3963
- trigger.type = "button";
3964
- trigger.className = "lb-mix-match__add-product";
3965
- trigger.setAttribute("data-step-trigger", String(i));
3966
- trigger.textContent = "Add a product";
3967
- group.appendChild(trigger);
3968
4126
  groups.appendChild(group);
3969
4127
  });
3970
4128
  shell.products.replaceWith(groups);
@@ -4001,6 +4159,7 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
4001
4159
  };
4002
4160
  function updateAll() {
4003
4161
  updateStepGroups(bodyDeps);
4162
+ updateDoor(bodyDeps);
4004
4163
  updatePricing3(
4005
4164
  bodyDeps,
4006
4165
  {
@@ -4047,10 +4206,14 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
4047
4206
  },
4048
4207
  canRemove: (item) => canRemovePick2(selections, item, rulesMap)
4049
4208
  });
4050
- shell.root.querySelectorAll("[data-step-trigger]").forEach((el2) => {
4209
+ door.addEventListener("click", () => {
4210
+ if (wizard.isOpen()) return;
4211
+ wizard.open(parseInt(door.getAttribute("data-step-door") ?? "", 10) || 0);
4212
+ });
4213
+ shell.root.querySelectorAll("[data-step-row]").forEach((el2) => {
4051
4214
  el2.addEventListener("click", () => {
4052
4215
  if (wizard.isOpen()) return;
4053
- wizard.open(parseInt(el2.getAttribute("data-step-trigger") ?? "", 10) || 0);
4216
+ wizard.open(parseInt(el2.getAttribute("data-step-row") ?? "", 10) || 0);
4054
4217
  });
4055
4218
  });
4056
4219
  shell.cta.addEventListener("click", () => {
@@ -6244,12 +6407,19 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6244
6407
  * Canon: WIDGET-DESIGN.md (spacing scale, radius system, chip family).
6245
6408
  */
6246
6409
 
6247
- /* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6410
+ /* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6411
+ One dashed door + one section per step. A step without picks shows a
6412
+ slim checklist row; a step with picks shows the heading + chosen
6413
+ cards. data-step-mode ("row" | "open") on the section picks the
6414
+ representation \u2014 both are in the markup, CSS hides the other, so the
6415
+ server-rendered untouched state IS the hydrated no-selection state.
6416
+ Spacing is per-pair rather than a flat gap: consecutive rows sit
6417
+ flush (hairline-separated), everything else breathes 16px. */
6248
6418
 
6249
6419
  .lb-multi-step__groups {
6250
6420
  display: flex;
6251
6421
  flex-direction: column;
6252
- gap: 16px;
6422
+ gap: 0;
6253
6423
  }
6254
6424
 
6255
6425
  .lb-multi-step__group {
@@ -6258,8 +6428,117 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6258
6428
  gap: 8px;
6259
6429
  }
6260
6430
 
6431
+ .lb-multi-step__group + .lb-multi-step__group {
6432
+ margin-top: 16px;
6433
+ }
6434
+
6435
+ /* A run of empty-step rows reads as one checklist: flush, hairlines
6436
+ between (the first row of a run keeps no border). */
6437
+ .lb-multi-step__group[data-step-mode="row"] + .lb-multi-step__group[data-step-mode="row"] {
6438
+ margin-top: 0;
6439
+ }
6440
+
6441
+ .lb-multi-step__group[data-step-mode="row"]
6442
+ + .lb-multi-step__group[data-step-mode="row"]
6443
+ .lb-multi-step__step-row {
6444
+ border-top: var(--lb-border-width) solid var(--lb-border);
6445
+ }
6446
+
6447
+ /* Mode switch: a row-mode step hides its open chrome and vice versa. */
6448
+ .lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-heading,
6449
+ .lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-slots {
6450
+ display: none;
6451
+ }
6452
+
6453
+ .lb-multi-step__group[data-step-mode="open"] .lb-multi-step__step-row {
6454
+ display: none;
6455
+ }
6456
+
6457
+ /* Checklist row \u2014 the empty step's whole representation, and a tap
6458
+ target that deep-links the wizard to that step. Quiet by design: the
6459
+ door above it is the primary action. */
6460
+ .lb-multi-step__step-row {
6461
+ display: flex;
6462
+ align-items: center;
6463
+ gap: 12px;
6464
+ width: 100%;
6465
+ min-height: 48px;
6466
+ padding: 10px 2px;
6467
+ background: none;
6468
+ border: none;
6469
+ font-family: inherit;
6470
+ text-align: left;
6471
+ cursor: pointer;
6472
+ box-sizing: border-box;
6473
+ transition: background-color 0.25s ease;
6474
+ }
6475
+
6476
+ .lb-multi-step__step-row:hover {
6477
+ background: color-mix(in srgb, var(--lb-text) 3%, transparent);
6478
+ }
6479
+
6480
+ .lb-multi-step__step-row:focus-visible {
6481
+ outline: 2px solid var(--lb-primary-color);
6482
+ outline-offset: -2px;
6483
+ border-radius: var(--lb-radius-sm);
6484
+ }
6485
+
6486
+ .lb-multi-step__step-row .lb-multi-step__group-count {
6487
+ margin-left: auto;
6488
+ }
6489
+
6490
+ /* Step number \u2014 quiet index, tabular so multi-digit lists align. */
6491
+ .lb-multi-step__step-ix {
6492
+ width: 18px;
6493
+ min-width: 18px;
6494
+ font-size: 12px;
6495
+ font-weight: 500;
6496
+ color: color-mix(in srgb, var(--lb-text) 45%, transparent);
6497
+ font-variant-numeric: tabular-nums;
6498
+ }
6499
+
6500
+ /* The door \u2014 the single wizard entry point, reusing the dashed
6501
+ add-product recipe with a two-line label. JS moves it directly above
6502
+ the first unsatisfied step and hides it once every step is met. The
6503
+ add-product recipe's own margin-top is for its mix & match position
6504
+ (below the slot list); here the pair rules own all spacing. */
6505
+ .lb-multi-step__door {
6506
+ margin-top: 0;
6507
+ }
6508
+
6509
+ .lb-multi-step__door + .lb-multi-step__group {
6510
+ margin-top: 12px;
6511
+ }
6512
+
6513
+ .lb-multi-step__door + .lb-multi-step__group[data-step-mode="row"] {
6514
+ margin-top: 4px;
6515
+ }
6516
+
6517
+ .lb-multi-step__group + .lb-multi-step__door {
6518
+ margin-top: 16px;
6519
+ }
6520
+
6521
+ /* A hidden door is parked as the container's first child (updateDoor), so
6522
+ the group after it is really the first visible element \u2014 no door gap. */
6523
+ .lb-multi-step__door--hidden:first-child + .lb-multi-step__group {
6524
+ margin-top: 0;
6525
+ }
6526
+
6527
+ .lb-multi-step__door-copy {
6528
+ display: flex;
6529
+ flex-direction: column;
6530
+ gap: 2px;
6531
+ }
6532
+
6533
+ .lb-multi-step__door-sub {
6534
+ font-size: 13px;
6535
+ font-weight: 400;
6536
+ line-height: 1.2;
6537
+ color: color-mix(in srgb, var(--lb-text) 55%, transparent);
6538
+ }
6539
+
6261
6540
  /* Heading is a quiet, non-interactive row: step name left, live count
6262
- chip right. The step's dashed trigger below is the click target. */
6541
+ chip right. The step's chosen cards below are the edit targets. */
6263
6542
  .lb-multi-step__group-heading {
6264
6543
  display: flex;
6265
6544
  align-items: center;
@@ -6742,6 +7021,7 @@ var LimeBundleElement = class extends HTMLElement {
6742
7021
  "storefront-token",
6743
7022
  "bundle-gid",
6744
7023
  "product-handle",
7024
+ "product-id",
6745
7025
  "app-url",
6746
7026
  "analytics",
6747
7027
  "locale",
@@ -6802,7 +7082,7 @@ var LimeBundleElement = class extends HTMLElement {
6802
7082
  }
6803
7083
  attributeChangedCallback(name, oldValue, newValue) {
6804
7084
  if (oldValue === newValue || !this.isConnected) return;
6805
- if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
7085
+ if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
6806
7086
  if (this.shopDomain && this.storefrontToken) {
6807
7087
  this.fetchBundle();
6808
7088
  }
@@ -6820,6 +7100,16 @@ var LimeBundleElement = class extends HTMLElement {
6820
7100
  get productHandleAttr() {
6821
7101
  return this.getAttribute("product-handle") ?? "";
6822
7102
  }
7103
+ /**
7104
+ * The current product's id — full GID (`gid://shopify/Product/123`) or
7105
+ * bare numeric id. Tells multi-product renderers (volume) which of the
7106
+ * bundle's products this embed is standing on; takes precedence over the
7107
+ * handle cascade. Without it (and with no resolvable handle) they bind to
7108
+ * the bundle's first product.
7109
+ */
7110
+ get productIdAttr() {
7111
+ return this.getAttribute("product-id") ?? "";
7112
+ }
6823
7113
  get appUrl() {
6824
7114
  return this.getAttribute("app-url") ?? "";
6825
7115
  }
@@ -6993,7 +7283,7 @@ var LimeBundleElement = class extends HTMLElement {
6993
7283
  });
6994
7284
  const storage = window.localStorage;
6995
7285
  const key = cartStorageKey(this.shopDomain);
6996
- const existingCartId = storage?.getItem(key) ?? null;
7286
+ let cartId = storage?.getItem(key) ?? null;
6997
7287
  const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
6998
7288
  const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
6999
7289
  const fail = (message) => {
@@ -7007,10 +7297,17 @@ var LimeBundleElement = class extends HTMLElement {
7007
7297
  };
7008
7298
  try {
7009
7299
  let checkoutUrl = null;
7010
- if (existingCartId) {
7300
+ if (cartId) {
7301
+ cartId = await this.clearAbandonedBundleLines(
7302
+ client,
7303
+ cartId,
7304
+ () => storage?.removeItem(key)
7305
+ );
7306
+ }
7307
+ if (cartId) {
7011
7308
  const res = await client.query(
7012
7309
  CART_LINES_ADD_MUTATION,
7013
- { cartId: existingCartId, lines }
7310
+ { cartId, lines }
7014
7311
  );
7015
7312
  const payload = res.cartLinesAdd;
7016
7313
  if (payload?.userErrors?.length) {
@@ -7063,6 +7360,42 @@ var LimeBundleElement = class extends HTMLElement {
7063
7360
  );
7064
7361
  }
7065
7362
  }
7363
+ /**
7364
+ * Remove this widget's own earlier lines — the ones tagged with the
7365
+ * `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
7366
+ * clicked add three times while deciding checks out with only what they
7367
+ * just built. Lines without the tag were put in the cart by something
7368
+ * else (the host site, a previous non-widget purchase flow) and are
7369
+ * left alone.
7370
+ *
7371
+ * Returns the cart id when the cart is still usable. Returns null after
7372
+ * calling `dropSavedCart` when it isn't: either the id no longer
7373
+ * resolves (expired or merged on Shopify's side), or the removal itself
7374
+ * failed — in both cases a fresh cart is the only way to guarantee the
7375
+ * checkout matches the shopper's current build.
7376
+ */
7377
+ async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
7378
+ const res = await client.query(CART_LINES_QUERY, {
7379
+ cartId
7380
+ });
7381
+ if (!res.cart) {
7382
+ dropSavedCart();
7383
+ return null;
7384
+ }
7385
+ const abandonedLineIds = res.cart.lines.nodes.filter(
7386
+ (line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
7387
+ ).map((line) => line.id);
7388
+ if (abandonedLineIds.length === 0) return cartId;
7389
+ const removal = await client.query(
7390
+ CART_LINES_REMOVE_MUTATION,
7391
+ { cartId, lineIds: abandonedLineIds }
7392
+ );
7393
+ if (removal.cartLinesRemove?.userErrors?.length) {
7394
+ dropSavedCart();
7395
+ return null;
7396
+ }
7397
+ return cartId;
7398
+ }
7066
7399
  reportAddToCartEvent(bundle, lines) {
7067
7400
  if (!this.analyticsEnabled || !this.appUrl) return;
7068
7401
  const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
@@ -7134,7 +7467,10 @@ var LimeBundleElement = class extends HTMLElement {
7134
7467
  );
7135
7468
  break;
7136
7469
  case "volume":
7137
- renderVolumeBundle(container, bundle, dispatch);
7470
+ renderVolumeBundle(container, bundle, dispatch, {
7471
+ productId: this.productIdAttr || null,
7472
+ productHandle: this.currentProductHandle
7473
+ });
7138
7474
  break;
7139
7475
  case "bogo":
7140
7476
  renderBogoBundle(container, bundle, dispatch, registerCleanup);