@lime-bundles/widget 4.5.0 → 4.6.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.d.ts CHANGED
@@ -109,17 +109,31 @@ declare class LimeBundleElement extends HTMLElement {
109
109
  private get country();
110
110
  private get language();
111
111
  private get marketId();
112
+ /**
113
+ * Store→presentment currency rate. Merchant-configured amounts
114
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
115
+ * the shop's store currency; set this to the buyer's currency rate so
116
+ * quoted sale prices match what the checkout's discount function
117
+ * charges. Anything unparseable or non-positive falls back to 1 —
118
+ * no conversion — via `normalizeCurrencyRate`.
119
+ */
120
+ private get currencyRate();
112
121
  /**
113
122
  * Returns the @inContext-wrapped query when any context field is set;
114
123
  * otherwise the plain query. Keeps responses publicly cacheable when
115
124
  * no buyer/country/language is set.
116
125
  */
117
126
  private wrapQueryForContext;
127
+ /** Buyer resolved once per fetch so the market gate can read the
128
+ * company location without invoking a callback resolver twice. */
129
+ private resolvedBuyer;
118
130
  /**
119
- * Returns true if this bundle should be hidden for the current market.
131
+ * Returns true if this bundle should be hidden for the current buyer.
120
132
  * For "all" bundles, always returns false (visible). For "specific"
121
- * bundles, returns true when no marketId was set or when the bundle's
122
- * marketIds doesn't include the configured market.
133
+ * bundles, visibility needs a matching signal: the `country` attribute
134
+ * against the bundle's projected country codes (retail markets), the
135
+ * resolved buyer's companyLocationId against its projected company
136
+ * locations (B2B markets), or a legacy `market-id` match.
123
137
  */
124
138
  private isMarketHidden;
125
139
  private fetchBundle;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  CART_LINES_REMOVE_MUTATION,
10
10
  SHOP_SETTINGS_QUERY,
11
11
  parseMetaobjectBundle,
12
+ convertBundleToPresentment,
12
13
  observeImpression,
13
14
  reportImpression,
14
15
  reportAddToCart,
@@ -16,7 +17,7 @@ import {
16
17
  sanitizeCustomCss,
17
18
  hasInContext,
18
19
  withInContext,
19
- isVisibleInMarket,
20
+ isVisibleToBuyer,
20
21
  getVisitorId,
21
22
  resolveAbTests
22
23
  } from "@lime-bundles/core";
@@ -3582,6 +3583,8 @@ function mergeCartLines(selections) {
3582
3583
  var ALL_TYPES2 = "__all__";
3583
3584
  var CLOSE_TRANSITION_MS2 = 300;
3584
3585
  var SEARCH_DEBOUNCE_MS2 = 200;
3586
+ var STEP_CASCADE_MS = 500;
3587
+ var AUTO_ADVANCE_HOLD_MS = 160;
3585
3588
  function createWizard(deps) {
3586
3589
  const { container, steps, selections, t } = deps;
3587
3590
  const overlay = container.querySelector("[data-modal-overlay]");
@@ -3606,6 +3609,8 @@ function createWizard(deps) {
3606
3609
  let modalOpen = false;
3607
3610
  let stepIndex = 0;
3608
3611
  let rows = [];
3612
+ let cascadeTimer = null;
3613
+ let autoAdvanceTimer = null;
3609
3614
  const productsFor = (i) => steps[i].eligibleProducts || [];
3610
3615
  const rowCapacity = {
3611
3616
  remainingSpots: () => stepHeadroom(selections, steps, stepIndex),
@@ -3706,7 +3711,13 @@ function createWizard(deps) {
3706
3711
  if (!deps.autoAdvance) return;
3707
3712
  if (stepIndex >= steps.length - 1) return;
3708
3713
  if (stepHeadroom(selections, steps, stepIndex) !== 0) return;
3709
- goToStep(stepIndex + 1);
3714
+ const from = stepIndex;
3715
+ if (autoAdvanceTimer !== null) clearTimeout(autoAdvanceTimer);
3716
+ autoAdvanceTimer = setTimeout(() => {
3717
+ autoAdvanceTimer = null;
3718
+ if (!modalOpen) return;
3719
+ goToStep(from + 1);
3720
+ }, AUTO_ADVANCE_HOLD_MS);
3710
3721
  }
3711
3722
  function buildProductList(forStep) {
3712
3723
  if (!modalList) return;
@@ -3894,7 +3905,29 @@ function createWizard(deps) {
3894
3905
  });
3895
3906
  }
3896
3907
  function goToStep(i, opts = {}) {
3897
- stepIndex = Math.max(0, Math.min(steps.length - 1, i));
3908
+ const target = Math.max(0, Math.min(steps.length - 1, i));
3909
+ if (autoAdvanceTimer !== null) {
3910
+ clearTimeout(autoAdvanceTimer);
3911
+ autoAdvanceTimer = null;
3912
+ }
3913
+ if (modalDialog) {
3914
+ if (cascadeTimer !== null) clearTimeout(cascadeTimer);
3915
+ cascadeTimer = null;
3916
+ modalDialog.removeAttribute("data-step-transition");
3917
+ if (opts.animate !== false && target !== stepIndex) {
3918
+ void modalDialog.offsetHeight;
3919
+ modalDialog.setAttribute(
3920
+ "data-step-transition",
3921
+ target > stepIndex ? "forward" : "back"
3922
+ );
3923
+ const dialog = modalDialog;
3924
+ cascadeTimer = setTimeout(() => {
3925
+ cascadeTimer = null;
3926
+ dialog.removeAttribute("data-step-transition");
3927
+ }, STEP_CASCADE_MS);
3928
+ }
3929
+ }
3930
+ stepIndex = target;
3898
3931
  buildProductList(stepIndex);
3899
3932
  buildFilters(stepIndex);
3900
3933
  activeType = ALL_TYPES2;
@@ -3919,7 +3952,10 @@ function createWizard(deps) {
3919
3952
  function open(atStep) {
3920
3953
  if (!overlay || modalOpen) return;
3921
3954
  modalOpen = true;
3922
- goToStep(typeof atStep === "number" ? atStep : 0, { skipFocus: true });
3955
+ goToStep(typeof atStep === "number" ? atStep : 0, {
3956
+ skipFocus: true,
3957
+ animate: false
3958
+ });
3923
3959
  overlay.style.display = "";
3924
3960
  void overlay.offsetHeight;
3925
3961
  overlay.classList.add("lb-mix-match__modal-overlay--open");
@@ -3932,6 +3968,14 @@ function createWizard(deps) {
3932
3968
  function close() {
3933
3969
  if (!overlay || !modalOpen) return;
3934
3970
  modalOpen = false;
3971
+ if (autoAdvanceTimer !== null) {
3972
+ clearTimeout(autoAdvanceTimer);
3973
+ autoAdvanceTimer = null;
3974
+ }
3975
+ if (cascadeTimer !== null) {
3976
+ clearTimeout(cascadeTimer);
3977
+ cascadeTimer = null;
3978
+ }
3935
3979
  overlay.classList.remove("lb-mix-match__modal-overlay--open");
3936
3980
  unlock();
3937
3981
  setTimeout(() => {
@@ -4319,11 +4363,18 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4319
4363
  --lb-list-max-height: 360px;
4320
4364
  /* Depth of the soft fade at a scroll list's clipped edge. */
4321
4365
  --lb-edge-fade-size: 20px;
4322
- /* Derived "small" corner radius for inner controls (thumbnails, variant
4323
- selects, the qty chip). One step tighter than the global --lb-radius so
4324
- small elements stay proportionate. Both come from the global radius
4325
- preset; nothing else sets a radius. */
4326
- --lb-radius-sm: max(0px, calc(var(--lb-radius) - 4px));
4366
+ /* Derived "small" corner radius for chips and badges \u2014 the low-stock badge,
4367
+ the volume tier badge, the multi-step count chip. Controls and media
4368
+ (thumbnails, selects, steppers, buttons) take the full --lb-radius: they
4369
+ are the elements the merchant is looking at when they pick a preset, and
4370
+ the picker's own help text already promises they follow it.
4371
+
4372
+ Proportional rather than a fixed subtraction. At "subtle" (6px) minus-4px
4373
+ left 2px, which reads as square \u2014 the merchant saw a picker set to Subtle
4374
+ render with sharp corners (issue #363). Two thirds holds the proportion at
4375
+ every preset: none 0, subtle 4px, rounded 8px, round 12px. No max() needed,
4376
+ since 0 \xD7 \u2154 is 0. */
4377
+ --lb-radius-sm: calc(var(--lb-radius) * 2 / 3);
4327
4378
 
4328
4379
  font-family: inherit;
4329
4380
  font-size: 16px;
@@ -4487,7 +4538,7 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
4487
4538
  ratio. Default keeps the historical 1:1 behaviour. */
4488
4539
  aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);
4489
4540
  background: var(--lb-thumbnail-bg);
4490
- border-radius: var(--lb-radius-sm);
4541
+ border-radius: var(--lb-radius);
4491
4542
  overflow: hidden;
4492
4543
  display: flex;
4493
4544
  align-items: center;
@@ -5037,12 +5088,12 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
5037
5088
  .lb-fixed .lb-bundle-thumbnail {
5038
5089
  width: 60px;
5039
5090
  min-width: 60px;
5040
- border-radius: var(--lb-radius-sm);
5091
+ border-radius: var(--lb-radius);
5041
5092
  box-sizing: border-box;
5042
5093
  }
5043
5094
 
5044
5095
  .lb-fixed .lb-bundle-thumbnail img {
5045
- border-radius: var(--lb-radius-sm);
5096
+ border-radius: var(--lb-radius);
5046
5097
  border: none;
5047
5098
  }
5048
5099
 
@@ -5059,7 +5110,7 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
5059
5110
  display: var(--lb-product-qty-chip-display, inline-flex);
5060
5111
  align-items: center;
5061
5112
  justify-content: center;
5062
- border-radius: var(--lb-radius-sm);
5113
+ border-radius: var(--lb-radius);
5063
5114
  background: color-mix(in srgb, var(--lb-text) 7%, transparent);
5064
5115
  color: color-mix(in srgb, var(--lb-text) 75%, transparent);
5065
5116
  font-size: 13px;
@@ -5075,7 +5126,7 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
5075
5126
  .lb-bundle-variant-select {
5076
5127
  display: inline-block;
5077
5128
  border: var(--lb-border-width) solid var(--lb-border);
5078
- border-radius: var(--lb-radius-sm);
5129
+ border-radius: var(--lb-radius);
5079
5130
  padding: 8px 32px 8px 12px;
5080
5131
  font-size: 12px;
5081
5132
  line-height: 16px;
@@ -5223,12 +5274,12 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5223
5274
  position: relative;
5224
5275
  width: 60px;
5225
5276
  min-width: 60px;
5226
- border-radius: var(--lb-radius-sm);
5277
+ border-radius: var(--lb-radius);
5227
5278
  box-sizing: border-box;
5228
5279
  }
5229
5280
 
5230
5281
  .lb-mix-match .lb-bundle-thumbnail img {
5231
- border-radius: var(--lb-radius-sm);
5282
+ border-radius: var(--lb-radius);
5232
5283
  border: none;
5233
5284
  }
5234
5285
 
@@ -5394,14 +5445,26 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5394
5445
  color: inherit;
5395
5446
  }
5396
5447
 
5397
- /* Derived "small" radius for the picker's inner controls (search, thumbnails,
5398
- variant select, qty stepper, Add/Done buttons), one step tighter than the
5399
- modal's --lb-picker-radius \u2014 the picker-scoped mirror of --lb-radius-sm in the
5400
- main widget. Defined on the overlay so the modal AND the variant listbox
5401
- (which portals up to [data-modal-overlay]) both inherit it. The modal shell
5402
- and product tiles keep the full --lb-picker-radius. */
5448
+ /* Derived "small" radius for the picker's chips and badges \u2014 the progress
5449
+ segments, the "Added" badge, the close button, the filter pills. The
5450
+ picker-scoped mirror of --lb-radius-sm, and proportional for the same reason
5451
+ (see bundle-base.css).
5452
+
5453
+ Everything the merchant would call a control or an image \u2014 the modal, search,
5454
+ product tiles, thumbnails, variant dropdown, quantity stepper and the
5455
+ buttons \u2014 takes the full --lb-picker-radius instead. That is what the
5456
+ editor's own Borders copy promises them, and the reduced tier had it
5457
+ rendering square at the Subtle preset (issue #363).
5458
+
5459
+ Declared on BOTH hosts the modal can live under. On the storefront it portals
5460
+ to [data-modal-overlay], which the variant listbox portals up to as well. In
5461
+ the admin widget editor there is no overlay at all \u2014 the preview renders the
5462
+ modal directly under .lb-bundle-widget \u2014 so scoping this to the overlay alone
5463
+ left the variable undefined there and every control using it fell back to
5464
+ square corners while the modal and tiles rounded correctly (issue #363). */
5465
+ .lb-bundle-widget,
5403
5466
  [data-modal-overlay] {
5404
- --lb-picker-radius-sm: max(0px, calc(var(--lb-picker-radius) - 4px));
5467
+ --lb-picker-radius-sm: calc(var(--lb-picker-radius) * 2 / 3);
5405
5468
  }
5406
5469
 
5407
5470
  /* === Modal Overlay === */
@@ -5498,7 +5561,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5498
5561
  border: none;
5499
5562
  cursor: pointer;
5500
5563
  color: var(--lb-picker-text);
5501
- border-radius: 8px;
5564
+ border-radius: var(--lb-picker-radius-sm);
5502
5565
  padding: 0;
5503
5566
  margin: -12px -12px 0 0;
5504
5567
  transition: background 0.25s ease;
@@ -5528,7 +5591,9 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5528
5591
  .lb-mix-match__filter {
5529
5592
  padding: 6px 14px;
5530
5593
  border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
5531
- border-radius: 999px;
5594
+ /* Pills followed the merchant's picker radius as of #363 \u2014 a fixed 999px
5595
+ read as a stray rounded shape in a picker set to None or Subtle. */
5596
+ border-radius: var(--lb-picker-radius-sm);
5532
5597
  background: transparent;
5533
5598
  color: var(--lb-picker-text);
5534
5599
  font-family: inherit;
@@ -5582,7 +5647,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5582
5647
  width: 100%;
5583
5648
  padding: 12px 40px 12px 44px;
5584
5649
  border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
5585
- border-radius: var(--lb-picker-radius-sm);
5650
+ border-radius: var(--lb-picker-radius);
5586
5651
  font-size: 16px;
5587
5652
  line-height: 20px;
5588
5653
  color: var(--lb-picker-text);
@@ -5682,7 +5747,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5682
5747
  independent from the main widget's thumbnailRatio so a merchant can
5683
5748
  e.g. show tall picker thumbs with square main thumbs. */
5684
5749
  aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);
5685
- border-radius: var(--lb-picker-radius-sm);
5750
+ border-radius: var(--lb-picker-radius);
5686
5751
  box-sizing: border-box;
5687
5752
  overflow: hidden;
5688
5753
  background: var(--lb-thumbnail-bg);
@@ -5695,7 +5760,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5695
5760
  to auto/contain so the image renders uncropped at intrinsic ratio. */
5696
5761
  height: var(--lb-picker-thumbnail-img-height, 100%);
5697
5762
  object-fit: var(--lb-picker-thumbnail-img-fit, cover);
5698
- border-radius: var(--lb-picker-radius-sm);
5763
+ border-radius: var(--lb-picker-radius);
5699
5764
  }
5700
5765
 
5701
5766
  .lb-mix-match__modal-product-info {
@@ -5770,7 +5835,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5770
5835
  font-size: 12px;
5771
5836
  padding: 4px 24px 4px 8px;
5772
5837
  border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
5773
- border-radius: var(--lb-picker-radius-sm);
5838
+ border-radius: var(--lb-picker-radius);
5774
5839
  background-color: var(--lb-picker-bg);
5775
5840
  background-image: var(--lb-picker-variant-chevron);
5776
5841
  background-repeat: no-repeat;
@@ -5821,7 +5886,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5821
5886
  width: 100%;
5822
5887
  flex-shrink: 0;
5823
5888
  border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
5824
- border-radius: var(--lb-picker-radius-sm);
5889
+ border-radius: var(--lb-picker-radius);
5825
5890
  background-color: var(--lb-picker-bg);
5826
5891
  overflow: hidden;
5827
5892
  box-sizing: border-box;
@@ -5898,7 +5963,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
5898
5963
  background: var(--lb-picker-add-bg);
5899
5964
  color: var(--lb-picker-add-label);
5900
5965
  border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);
5901
- border-radius: var(--lb-picker-radius-sm);
5966
+ border-radius: var(--lb-picker-radius);
5902
5967
  box-sizing: border-box;
5903
5968
  font-size: 12px;
5904
5969
  font-weight: 600;
@@ -6039,7 +6104,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
6039
6104
  background: var(--lb-picker-add-bg);
6040
6105
  color: var(--lb-picker-add-label);
6041
6106
  border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);
6042
- border-radius: var(--lb-picker-radius-sm);
6107
+ border-radius: var(--lb-picker-radius);
6043
6108
  box-sizing: border-box;
6044
6109
  font-family: inherit;
6045
6110
  font-size: 14px;
@@ -6088,7 +6153,9 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
6088
6153
  .lb-mix-match__modal {
6089
6154
  max-width: 100%;
6090
6155
  max-height: 90vh;
6091
- border-radius: 16px 16px 0 0;
6156
+ /* Bottom sheet: only the top corners round, but they round to whatever the
6157
+ merchant picked rather than a fixed 16px (issue #363). */
6158
+ border-radius: var(--lb-picker-radius) var(--lb-picker-radius) 0 0;
6092
6159
  transform: translateY(100%);
6093
6160
  }
6094
6161
 
@@ -6355,12 +6422,12 @@ var BUNDLE_BOGO_CSS = `/* Lime Bundles \u2014 BOGO (Buy X Get Y) bundle styles *
6355
6422
  .lb-bogo .lb-bundle-thumbnail {
6356
6423
  width: 60px;
6357
6424
  min-width: 60px;
6358
- border-radius: var(--lb-radius-sm);
6425
+ border-radius: var(--lb-radius);
6359
6426
  box-sizing: border-box;
6360
6427
  }
6361
6428
 
6362
6429
  .lb-bogo .lb-bundle-thumbnail img {
6363
- border-radius: var(--lb-radius-sm);
6430
+ border-radius: var(--lb-radius);
6364
6431
  border: none;
6365
6432
  }
6366
6433
 
@@ -6481,6 +6548,13 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6481
6548
  cursor: pointer;
6482
6549
  box-sizing: border-box;
6483
6550
  transition: background-color 0.25s ease;
6551
+ /* Deliberately no border-radius. This row paints the checklist hairline as
6552
+ its own border-top (see the run rule above), and a radius tapers that
6553
+ border into the corner arcs \u2014 the dividers came out visibly curved at the
6554
+ "round" preset (issue #363). A full-bleed list row is meant to be square
6555
+ anyway; the hover tint below spans the card width, so it has no corner to
6556
+ round against. The :focus-visible radius is fine: an outline is a
6557
+ separate paint and never touches the border. */
6484
6558
  }
6485
6559
 
6486
6560
  .lb-multi-step__step-row:hover {
@@ -6628,7 +6702,7 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6628
6702
  padding: 10px 24px;
6629
6703
  background: none;
6630
6704
  border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);
6631
- border-radius: var(--lb-picker-radius-sm);
6705
+ border-radius: var(--lb-picker-radius);
6632
6706
  box-sizing: border-box;
6633
6707
  color: var(--lb-picker-text);
6634
6708
  font-family: inherit;
@@ -6646,6 +6720,108 @@ var BUNDLE_MULTI_STEP_CSS = `/**
6646
6720
  outline: 2px solid var(--lb-primary-color);
6647
6721
  outline-offset: 2px;
6648
6722
  }
6723
+
6724
+ /* \u2500\u2500 Step-change transition \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
6725
+ Moving between steps swaps the whole step-scoped region in a single
6726
+ paint, which reads as nothing happening (or as a glitch) rather than
6727
+ as arriving somewhere new \u2014 and worst of all under auto-advance,
6728
+ where the shopper never clicked anything. So the step assembles
6729
+ instead of arriving whole: the step label leads, the filter pills
6730
+ follow, then the product cards one after another. Staging is what
6731
+ makes the change legible; a uniform slide of everything at once only
6732
+ wobbles the card.
6733
+
6734
+ The wizard stamps data-step-transition="forward" | "back" on the
6735
+ dialog on every step change (manual Next/Back and auto-advance
6736
+ alike, never on open, where the modal entrance already carries the
6737
+ motion), and clears it again once the cascade has played \u2014 see
6738
+ STEP_CASCADE_MS in multi-step/wizard.ts. That clear is load-bearing,
6739
+ not tidiness: search and filter hide cards with .lb-hidden
6740
+ (display: none), and restoring display restarts a card's animation,
6741
+ so a live attribute would replay the whole cascade on every
6742
+ keystroke that reveals a card.
6743
+
6744
+ Two elements deliberately hold still. The footer, because Back/Next
6745
+ are the shopper's anchor while everything above them moves. And the
6746
+ progress rail, because it is the one thing that answers "where am I
6747
+ now" \u2014 animating the shopper's only orientation cue is what made the
6748
+ previous version hard to read. Its segment fill carries its own
6749
+ state change already. */
6750
+ @keyframes lb-multi-step-cascade-fwd {
6751
+ from {
6752
+ opacity: 0;
6753
+ transform: translateY(10px);
6754
+ }
6755
+ }
6756
+
6757
+ @keyframes lb-multi-step-cascade-back {
6758
+ from {
6759
+ opacity: 0;
6760
+ transform: translateY(-10px);
6761
+ }
6762
+ }
6763
+
6764
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle {
6765
+ animation: lb-multi-step-cascade-fwd 0.2s ease-out both;
6766
+ }
6767
+
6768
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__filters {
6769
+ animation: lb-multi-step-cascade-fwd 0.2s ease-out 0.05s both;
6770
+ }
6771
+
6772
+ /* Cards stagger 30ms apart, but only the first four: a step can hold
6773
+ dozens of products and the two-column grid shows about four at a
6774
+ time, so past that the stagger is below the fold and would only add
6775
+ lag. Everything from the fifth on shares the fourth's landing. */
6776
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > * {
6777
+ animation: lb-multi-step-cascade-fwd 0.22s ease-out 0.2s both;
6778
+ }
6779
+
6780
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(1) {
6781
+ animation-delay: 0.09s;
6782
+ }
6783
+
6784
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(2) {
6785
+ animation-delay: 0.12s;
6786
+ }
6787
+
6788
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(3) {
6789
+ animation-delay: 0.15s;
6790
+ }
6791
+
6792
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(4) {
6793
+ animation-delay: 0.18s;
6794
+ }
6795
+
6796
+ /* Going back runs the same cascade in the same order \u2014 only the offset
6797
+ flips, so the shopper still reads which way they moved. Overrides
6798
+ the name alone; the staggered delays above must survive. */
6799
+ .lb-mix-match__modal[data-step-transition="back"] .lb-mix-match__modal-subtitle,
6800
+ .lb-mix-match__modal[data-step-transition="back"] .lb-mix-match__filters,
6801
+ .lb-mix-match__modal[data-step-transition="back"] .lb-mix-match__modal-list > * {
6802
+ animation-name: lb-multi-step-cascade-back;
6803
+ }
6804
+
6805
+ @keyframes lb-multi-step-cascade-fade {
6806
+ from {
6807
+ opacity: 0;
6808
+ }
6809
+ }
6810
+
6811
+ /* Reduced motion keeps the fade and drops the travel and the stagger:
6812
+ the step change still has to be perceptible, which is the whole
6813
+ point of this block. The card selector is :nth-child(n) rather than
6814
+ * on purpose \u2014 a plain * loses to the :nth-child(1..4) delays above
6815
+ on specificity, which would leave the stagger running with nothing
6816
+ moving. :nth-child(n) matches every child at equal specificity and
6817
+ wins on order. */
6818
+ @media (prefers-reduced-motion: reduce) {
6819
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle,
6820
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__filters,
6821
+ .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(n) {
6822
+ animation: lb-multi-step-cascade-fade 0.15s ease-out both;
6823
+ }
6824
+ }
6649
6825
  `;
6650
6826
  var BUNDLE_DROPDOWN_CSS = `/**
6651
6827
  * Lime Bundles \u2014 Custom variant-picker dropdown styling.
@@ -6693,7 +6869,7 @@ var BUNDLE_DROPDOWN_CSS = `/**
6693
6869
  gap: 8px;
6694
6870
  width: 100%;
6695
6871
  border: var(--lb-border-width) solid var(--lb-border);
6696
- border-radius: var(--lb-radius-sm);
6872
+ border-radius: var(--lb-radius);
6697
6873
  padding: 8px 12px;
6698
6874
  font-size: 12px;
6699
6875
  line-height: 16px;
@@ -6756,7 +6932,7 @@ var BUNDLE_DROPDOWN_CSS = `/**
6756
6932
  background: var(--lb-bg);
6757
6933
  color: var(--lb-text);
6758
6934
  border: var(--lb-border-width) solid var(--lb-border);
6759
- border-radius: var(--lb-radius-sm);
6935
+ border-radius: var(--lb-radius);
6760
6936
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
6761
6937
  overflow-y: auto;
6762
6938
  overflow-x: hidden;
@@ -6817,6 +6993,10 @@ var BUNDLE_DROPDOWN_CSS = `/**
6817
6993
  overflow: hidden;
6818
6994
  text-overflow: ellipsis;
6819
6995
  color: var(--lb-text);
6996
+ /* The active/hover tint below fills the option, so it rounds with the rest
6997
+ of the widget rather than squaring off against the listbox corner it sits
6998
+ in (issue #363). */
6999
+ border-radius: var(--lb-radius-sm);
6820
7000
  }
6821
7001
 
6822
7002
  .lb-dropdown-option[aria-selected="true"] {
@@ -6875,7 +7055,7 @@ var BUNDLE_DROPDOWN_CSS = `/**
6875
7055
  .lb-mix-match__modal .lb-dropdown-trigger {
6876
7056
  border-color: var(--lb-picker-border-color);
6877
7057
  border-width: var(--lb-picker-border-width);
6878
- border-radius: var(--lb-picker-radius-sm);
7058
+ border-radius: var(--lb-picker-radius);
6879
7059
  background: var(--lb-picker-bg);
6880
7060
  color: var(--lb-picker-text);
6881
7061
  /* Match the picker qty stepper + Add button height (border-box so 32px is
@@ -6896,13 +7076,17 @@ var BUNDLE_DROPDOWN_CSS = `/**
6896
7076
  [data-modal-overlay] > .lb-dropdown-listbox {
6897
7077
  border-color: var(--lb-picker-border-color);
6898
7078
  border-width: var(--lb-picker-border-width);
6899
- border-radius: var(--lb-picker-radius-sm);
7079
+ border-radius: var(--lb-picker-radius);
6900
7080
  background: var(--lb-picker-bg);
6901
7081
  color: var(--lb-picker-text);
6902
7082
  scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)
6903
7083
  color-mix(in srgb, var(--lb-picker-text) 2%, transparent);
6904
7084
  }
6905
7085
 
7086
+ [data-modal-overlay] > .lb-dropdown-listbox .lb-dropdown-option {
7087
+ border-radius: var(--lb-picker-radius-sm);
7088
+ }
7089
+
6906
7090
  [data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {
6907
7091
  background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);
6908
7092
  border-radius: 2px;
@@ -7037,7 +7221,8 @@ var LimeBundleElement = class extends HTMLElement {
7037
7221
  "locale",
7038
7222
  "country",
7039
7223
  "language",
7040
- "market-id"
7224
+ "market-id",
7225
+ "currency-rate"
7041
7226
  ];
7042
7227
  /**
7043
7228
  * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
@@ -7092,7 +7277,7 @@ var LimeBundleElement = class extends HTMLElement {
7092
7277
  }
7093
7278
  attributeChangedCallback(name, oldValue, newValue) {
7094
7279
  if (oldValue === newValue || !this.isConnected) return;
7095
- if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
7280
+ if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token" || name === "currency-rate") {
7096
7281
  if (this.shopDomain && this.storefrontToken) {
7097
7282
  this.fetchBundle();
7098
7283
  }
@@ -7135,6 +7320,17 @@ var LimeBundleElement = class extends HTMLElement {
7135
7320
  get marketId() {
7136
7321
  return this.getAttribute("market-id") ?? void 0;
7137
7322
  }
7323
+ /**
7324
+ * Store→presentment currency rate. Merchant-configured amounts
7325
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
7326
+ * the shop's store currency; set this to the buyer's currency rate so
7327
+ * quoted sale prices match what the checkout's discount function
7328
+ * charges. Anything unparseable or non-positive falls back to 1 —
7329
+ * no conversion — via `normalizeCurrencyRate`.
7330
+ */
7331
+ get currencyRate() {
7332
+ return this.getAttribute("currency-rate") ?? void 0;
7333
+ }
7138
7334
  /**
7139
7335
  * Returns the @inContext-wrapped query when any context field is set;
7140
7336
  * otherwise the plain query. Keeps responses publicly cacheable when
@@ -7149,14 +7345,23 @@ var LimeBundleElement = class extends HTMLElement {
7149
7345
  buyer: this.buyer
7150
7346
  }) ? withInContext(query) : query;
7151
7347
  }
7348
+ /** Buyer resolved once per fetch so the market gate can read the
7349
+ * company location without invoking a callback resolver twice. */
7350
+ resolvedBuyer = void 0;
7152
7351
  /**
7153
- * Returns true if this bundle should be hidden for the current market.
7352
+ * Returns true if this bundle should be hidden for the current buyer.
7154
7353
  * For "all" bundles, always returns false (visible). For "specific"
7155
- * bundles, returns true when no marketId was set or when the bundle's
7156
- * marketIds doesn't include the configured market.
7354
+ * bundles, visibility needs a matching signal: the `country` attribute
7355
+ * against the bundle's projected country codes (retail markets), the
7356
+ * resolved buyer's companyLocationId against its projected company
7357
+ * locations (B2B markets), or a legacy `market-id` match.
7157
7358
  */
7158
7359
  isMarketHidden(bundle) {
7159
- return !isVisibleInMarket(bundle, this.marketId);
7360
+ return !isVisibleToBuyer(bundle, {
7361
+ marketId: this.marketId,
7362
+ countryCode: this.country,
7363
+ companyLocationId: this.resolvedBuyer?.companyLocationId
7364
+ });
7160
7365
  }
7161
7366
  async fetchBundle() {
7162
7367
  if (!this.shopDomain || !this.storefrontToken) {
@@ -7169,12 +7374,17 @@ var LimeBundleElement = class extends HTMLElement {
7169
7374
  const controller = new AbortController();
7170
7375
  this.abortController = controller;
7171
7376
  this.renderLoading();
7377
+ try {
7378
+ this.resolvedBuyer = typeof this.buyer === "function" ? await this.buyer() : this.buyer;
7379
+ } catch {
7380
+ this.resolvedBuyer = void 0;
7381
+ }
7172
7382
  const client = createStorefrontClient({
7173
7383
  shopDomain: this.shopDomain,
7174
7384
  accessToken: this.storefrontToken,
7175
7385
  country: this.country,
7176
7386
  language: this.language,
7177
- buyer: this.buyer
7387
+ buyer: this.resolvedBuyer
7178
7388
  });
7179
7389
  try {
7180
7390
  let bundlePromise;
@@ -7239,7 +7449,7 @@ var LimeBundleElement = class extends HTMLElement {
7239
7449
  data.metaobject.id,
7240
7450
  data.metaobject.fields
7241
7451
  );
7242
- this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
7452
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [convertBundleToPresentment(parsed, this.currencyRate)] : [];
7243
7453
  }
7244
7454
  async fetchProductBundles(client, signal, productHandle) {
7245
7455
  const data = await client.query(
@@ -7255,7 +7465,9 @@ var LimeBundleElement = class extends HTMLElement {
7255
7465
  const bundles = [];
7256
7466
  for (const ref of refs) {
7257
7467
  const parsed = parseMetaobjectBundle(ref.id, ref.fields);
7258
- if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
7468
+ if (parsed && !this.isMarketHidden(parsed)) {
7469
+ bundles.push(convertBundleToPresentment(parsed, this.currencyRate));
7470
+ }
7259
7471
  }
7260
7472
  this.bundles = resolveAbTests(bundles, getVisitorId());
7261
7473
  }