@lime-bundles/widget 3.2.1 → 3.3.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.cts CHANGED
@@ -1,5 +1,64 @@
1
+ import { BuyerResolver } from '@lime-bundles/core';
2
+
3
+ /**
4
+ * <lime-bundle> Web Component — renders Lime Bundles on any storefront.
5
+ *
6
+ * ## The two modes
7
+ *
8
+ * Single-bundle (pinned):
9
+ * <lime-bundle
10
+ * shop-domain="my-shop.myshopify.com"
11
+ * storefront-token="shpat_..."
12
+ * bundle-gid="gid://shopify/Metaobject/42"
13
+ * ></lime-bundle>
14
+ *
15
+ * Product-aware (matches classic Liquid theme block behaviour — one snippet
16
+ * on the product page renders every active bundle configured against that
17
+ * product):
18
+ * <lime-bundle
19
+ * shop-domain="my-shop.myshopify.com"
20
+ * storefront-token="shpat_..."
21
+ * ></lime-bundle>
22
+ *
23
+ * Product resolution cascade (when no `bundle-gid` is set):
24
+ * 1. explicit `product-handle` attribute
25
+ * 2. <meta name="shopify:product-handle" content="..."> on the page
26
+ * 3. /products/<handle> segment of window.location.pathname
27
+ * 4. fallthrough: renders nothing, fires `lime-bundle:error`
28
+ *
29
+ * ## Add-to-cart behaviour
30
+ *
31
+ * Merchants who do nothing get a default: the widget calls Shopify's
32
+ * Storefront Cart API (tokenless — no extra scopes required) and redirects
33
+ * the browser to the returned checkoutUrl. One-click-to-checkout is the
34
+ * right UX for most merchants pasting the widget into Webflow / Wix /
35
+ * Squarespace / static HTML.
36
+ *
37
+ * Merchants with their own cart state (Hydrogen's useCart, a custom cart
38
+ * drawer, etc.) opt out by attaching a listener that calls
39
+ * `event.preventDefault()`:
40
+ *
41
+ * document.querySelector("lime-bundle").addEventListener(
42
+ * "lime-bundle:add-to-cart",
43
+ * (ev) => {
44
+ * ev.preventDefault(); // suppress the default redirect
45
+ * myCart.linesAdd(ev.detail.lines);
46
+ * },
47
+ * );
48
+ *
49
+ * The event is always dispatched; only the default action is conditional.
50
+ */
51
+
1
52
  declare class LimeBundleElement extends HTMLElement {
2
53
  static observedAttributes: string[];
54
+ /**
55
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
56
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
57
+ * because the customer access token would land in DOM snapshots (Sentry,
58
+ * analytics, browser extensions) and Referer headers. See the package
59
+ * README ("Markets & B2B").
60
+ */
61
+ buyer: BuyerResolver | undefined;
3
62
  private shadow;
4
63
  private bundles;
5
64
  private abortController;
@@ -28,6 +87,22 @@ declare class LimeBundleElement extends HTMLElement {
28
87
  private get productHandleAttr();
29
88
  private get appUrl();
30
89
  private get analyticsEnabled();
90
+ private get country();
91
+ private get language();
92
+ private get marketId();
93
+ /**
94
+ * Returns the @inContext-wrapped query when any context field is set;
95
+ * otherwise the plain query. Keeps responses publicly cacheable when
96
+ * no buyer/country/language is set.
97
+ */
98
+ private wrapQueryForContext;
99
+ /**
100
+ * Returns true if this bundle should be hidden for the current market.
101
+ * For "all" bundles, always returns false (visible). For "specific"
102
+ * bundles, returns true when no marketId was set or when the bundle's
103
+ * marketIds doesn't include the configured market.
104
+ */
105
+ private isMarketHidden;
31
106
  private fetchBundle;
32
107
  /**
33
108
  * Resolve the visitor's A/B bucket for every bundle with an active test
package/dist/index.d.ts CHANGED
@@ -1,5 +1,64 @@
1
+ import { BuyerResolver } from '@lime-bundles/core';
2
+
3
+ /**
4
+ * <lime-bundle> Web Component — renders Lime Bundles on any storefront.
5
+ *
6
+ * ## The two modes
7
+ *
8
+ * Single-bundle (pinned):
9
+ * <lime-bundle
10
+ * shop-domain="my-shop.myshopify.com"
11
+ * storefront-token="shpat_..."
12
+ * bundle-gid="gid://shopify/Metaobject/42"
13
+ * ></lime-bundle>
14
+ *
15
+ * Product-aware (matches classic Liquid theme block behaviour — one snippet
16
+ * on the product page renders every active bundle configured against that
17
+ * product):
18
+ * <lime-bundle
19
+ * shop-domain="my-shop.myshopify.com"
20
+ * storefront-token="shpat_..."
21
+ * ></lime-bundle>
22
+ *
23
+ * Product resolution cascade (when no `bundle-gid` is set):
24
+ * 1. explicit `product-handle` attribute
25
+ * 2. <meta name="shopify:product-handle" content="..."> on the page
26
+ * 3. /products/<handle> segment of window.location.pathname
27
+ * 4. fallthrough: renders nothing, fires `lime-bundle:error`
28
+ *
29
+ * ## Add-to-cart behaviour
30
+ *
31
+ * Merchants who do nothing get a default: the widget calls Shopify's
32
+ * Storefront Cart API (tokenless — no extra scopes required) and redirects
33
+ * the browser to the returned checkoutUrl. One-click-to-checkout is the
34
+ * right UX for most merchants pasting the widget into Webflow / Wix /
35
+ * Squarespace / static HTML.
36
+ *
37
+ * Merchants with their own cart state (Hydrogen's useCart, a custom cart
38
+ * drawer, etc.) opt out by attaching a listener that calls
39
+ * `event.preventDefault()`:
40
+ *
41
+ * document.querySelector("lime-bundle").addEventListener(
42
+ * "lime-bundle:add-to-cart",
43
+ * (ev) => {
44
+ * ev.preventDefault(); // suppress the default redirect
45
+ * myCart.linesAdd(ev.detail.lines);
46
+ * },
47
+ * );
48
+ *
49
+ * The event is always dispatched; only the default action is conditional.
50
+ */
51
+
1
52
  declare class LimeBundleElement extends HTMLElement {
2
53
  static observedAttributes: string[];
54
+ /**
55
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
56
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
57
+ * because the customer access token would land in DOM snapshots (Sentry,
58
+ * analytics, browser extensions) and Referer headers. See the package
59
+ * README ("Markets & B2B").
60
+ */
61
+ buyer: BuyerResolver | undefined;
3
62
  private shadow;
4
63
  private bundles;
5
64
  private abortController;
@@ -28,6 +87,22 @@ declare class LimeBundleElement extends HTMLElement {
28
87
  private get productHandleAttr();
29
88
  private get appUrl();
30
89
  private get analyticsEnabled();
90
+ private get country();
91
+ private get language();
92
+ private get marketId();
93
+ /**
94
+ * Returns the @inContext-wrapped query when any context field is set;
95
+ * otherwise the plain query. Keeps responses publicly cacheable when
96
+ * no buyer/country/language is set.
97
+ */
98
+ private wrapQueryForContext;
99
+ /**
100
+ * Returns true if this bundle should be hidden for the current market.
101
+ * For "all" bundles, always returns false (visible). For "specific"
102
+ * bundles, returns true when no marketId was set or when the bundle's
103
+ * marketIds doesn't include the configured market.
104
+ */
105
+ private isMarketHidden;
31
106
  private fetchBundle;
32
107
  /**
33
108
  * Resolve the visitor's A/B bucket for every bundle with an active test
package/dist/index.js CHANGED
@@ -13,7 +13,9 @@ import {
13
13
  injectCustomCss,
14
14
  sanitizeCustomCss,
15
15
  getABTestAssignment,
16
- applyABVariantB
16
+ applyABVariantB,
17
+ hasInContext,
18
+ withInContext
17
19
  } from "@lime-bundles/core";
18
20
 
19
21
  // src/renderers/fixed.ts
@@ -142,6 +144,7 @@ function bindDropdown(select) {
142
144
  let optionEls = [];
143
145
  let instance;
144
146
  function syncFromSelect() {
147
+ trigger.disabled = select.disabled;
145
148
  const opts = readOptions(select);
146
149
  const idx = select.selectedIndex;
147
150
  triggerLabel.textContent = idx >= 0 && opts[idx] ? opts[idx].label : "";
@@ -1051,20 +1054,26 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
1051
1054
  root.appendChild(pricingSection.el);
1052
1055
  const savingsBar = wc.savingsBar.visible ? renderSavingsBar2() : null;
1053
1056
  if (savingsBar) root.appendChild(savingsBar.el);
1057
+ const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
1058
+ cta.disabled = true;
1059
+ cta.addEventListener("click", () => {
1060
+ if (cta.disabled) return;
1061
+ onAddToCart(buildCartLines(selections, bundle));
1062
+ });
1054
1063
  const modal = renderModal(bundle, eligible, currency, {
1055
1064
  showSearch: wc.showSearch,
1056
1065
  showQtySelector,
1057
1066
  selections,
1058
1067
  onAdd: (product, variant, quantity) => addSelection(product, variant, quantity),
1059
- isComplete: () => selections.length >= requiredQty
1068
+ isComplete: () => selections.length >= requiredQty,
1069
+ getFocusAfterAdd: () => {
1070
+ const empty = slotsContainer.querySelector(
1071
+ ".lb-mix-match__slot--empty"
1072
+ );
1073
+ return empty ?? cta;
1074
+ }
1060
1075
  });
1061
1076
  root.appendChild(modal.el);
1062
- const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
1063
- cta.disabled = true;
1064
- cta.addEventListener("click", () => {
1065
- if (cta.disabled) return;
1066
- onAddToCart(buildCartLines(selections, bundle));
1067
- });
1068
1077
  root.appendChild(cta);
1069
1078
  root.appendChild(
1070
1079
  el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
@@ -1196,12 +1205,14 @@ function renderProgress(requiredQty) {
1196
1205
  const wrap = el("div", "lb-mix-match__progress");
1197
1206
  const labels = el("div", "lb-mix-match__progress-labels");
1198
1207
  const count = el("span", "lb-mix-match__progress-count", {
1199
- "data-progress-count": ""
1208
+ "data-progress-count": "",
1209
+ "aria-live": "polite"
1200
1210
  });
1201
1211
  count.textContent = `0 of ${requiredQty} selected`;
1202
1212
  labels.appendChild(count);
1203
1213
  const remaining = el("span", "lb-mix-match__progress-remaining", {
1204
- "data-progress-remaining": ""
1214
+ "data-progress-remaining": "",
1215
+ "aria-live": "polite"
1205
1216
  });
1206
1217
  remaining.textContent = `${requiredQty} more to go`;
1207
1218
  labels.appendChild(remaining);
@@ -1576,12 +1587,12 @@ function renderModal(bundle, eligible, currency, handlers) {
1576
1587
  refreshFromSelections: () => {
1577
1588
  }
1578
1589
  };
1590
+ const optionSelects = [];
1579
1591
  if (availableVariants.length > 1) {
1580
1592
  const optionNames = availableVariants[0].selectedOptions.map(
1581
1593
  (o) => o.name
1582
1594
  );
1583
1595
  const productIdTail = ep.product.id.replace(/^.*\//, "");
1584
- const optionSelects = [];
1585
1596
  const resolveVariant = (values) => availableVariants.find(
1586
1597
  (v) => v.selectedOptions.length === values.length && v.selectedOptions.every((o, i) => o.value === values[i])
1587
1598
  ) ?? null;
@@ -1690,7 +1701,13 @@ function renderModal(bundle, eligible, currency, handlers) {
1690
1701
  productEl.appendChild(info);
1691
1702
  let addBtn = null;
1692
1703
  const refreshAddState = () => {
1693
- if (!addBtn) return;
1704
+ if (!addBtn) {
1705
+ optionSelects.forEach((sel) => {
1706
+ sel.disabled = true;
1707
+ });
1708
+ if (stepper) stepper.setExternallyDisabled(true);
1709
+ return;
1710
+ }
1694
1711
  const { cap } = computeStepperBounds();
1695
1712
  const already = alreadyInBundleFor(
1696
1713
  handlers.selections,
@@ -1709,12 +1726,19 @@ function renderModal(bundle, eligible, currency, handlers) {
1709
1726
  if (productInBundle) {
1710
1727
  addBtn.disabled = true;
1711
1728
  addBtn.textContent = "Added";
1729
+ addBtn.setAttribute("aria-label", `Added ${ep.product.title}`);
1712
1730
  productEl.classList.add("lb-mix-match__modal-product--in-bundle");
1713
1731
  } else {
1714
1732
  addBtn.textContent = "Add";
1733
+ addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
1715
1734
  productEl.classList.remove("lb-mix-match__modal-product--in-bundle");
1716
1735
  addBtn.disabled = handlers.isComplete() || cap < rule.min;
1717
1736
  }
1737
+ const rowDisabled = addBtn.disabled;
1738
+ optionSelects.forEach((sel) => {
1739
+ sel.disabled = rowDisabled;
1740
+ });
1741
+ if (stepper) stepper.setExternallyDisabled(rowDisabled);
1718
1742
  };
1719
1743
  if (!ep.isOos) {
1720
1744
  const actions = el("div", "lb-mix-match__modal-product-actions");
@@ -1723,12 +1747,15 @@ function renderModal(bundle, eligible, currency, handlers) {
1723
1747
  addBtn.type = "button";
1724
1748
  addBtn.className = "lb-mix-match__modal-add";
1725
1749
  addBtn.textContent = "Add";
1750
+ addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
1726
1751
  addBtn.addEventListener("click", () => {
1727
1752
  if (!addBtn || addBtn.disabled) return;
1728
1753
  if (handlers.isComplete()) return;
1729
1754
  const qty = stepper ? stepper.value() : rule.min;
1730
1755
  handlers.onAdd(ep.product, currentVariant, qty);
1731
1756
  if (stepper) stepper.reset(rule.min);
1757
+ const nextFocus = handlers.getFocusAfterAdd();
1758
+ if (nextFocus) lastFocused = nextFocus;
1732
1759
  close();
1733
1760
  });
1734
1761
  actions.appendChild(addBtn);
@@ -1824,6 +1851,7 @@ function renderQtyStepper(opts) {
1824
1851
  plus.innerHTML = STEPPER_PLUS_SVG;
1825
1852
  wrap.appendChild(plus);
1826
1853
  let current = clamp2(opts.initial, opts.getBounds());
1854
+ let externallyDisabled = false;
1827
1855
  function clamp2(n, b) {
1828
1856
  return Math.max(b.min, Math.min(b.max, n));
1829
1857
  }
@@ -1831,8 +1859,8 @@ function renderQtyStepper(opts) {
1831
1859
  const b = opts.getBounds();
1832
1860
  current = clamp2(current, b);
1833
1861
  valueEl.textContent = String(current);
1834
- minus.disabled = current <= b.min;
1835
- plus.disabled = current >= b.max;
1862
+ minus.disabled = externallyDisabled || current <= b.min;
1863
+ plus.disabled = externallyDisabled || current >= b.max;
1836
1864
  }
1837
1865
  minus.addEventListener("click", () => {
1838
1866
  const b = opts.getBounds();
@@ -1852,7 +1880,11 @@ function renderQtyStepper(opts) {
1852
1880
  current = clamp2(qty, opts.getBounds());
1853
1881
  paint();
1854
1882
  },
1855
- refresh: paint
1883
+ refresh: paint,
1884
+ setExternallyDisabled(disabled) {
1885
+ externallyDisabled = disabled;
1886
+ paint();
1887
+ }
1856
1888
  };
1857
1889
  }
1858
1890
  function buildEligibleProducts(bundle, oosBehavior) {
@@ -2499,6 +2531,13 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
2499
2531
  text-decoration: underline;
2500
2532
  }
2501
2533
 
2534
+ a.lb-bundle-product-name:focus-visible {
2535
+ outline: 2px solid var(--lb-primary-color);
2536
+ outline-offset: 0;
2537
+ box-shadow: none;
2538
+ border-radius: 2px;
2539
+ }
2540
+
2502
2541
  .lb-bundle-product-price {
2503
2542
  /* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
2504
2543
  display: var(--lb-product-price-display, block);
@@ -2947,6 +2986,7 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2947
2986
  .lb-bundle-variant-select:focus-visible {
2948
2987
  outline: 2px solid var(--lb-primary-color);
2949
2988
  outline-offset: 2px;
2989
+ box-shadow: none;
2950
2990
  }
2951
2991
  `;
2952
2992
  var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
@@ -3024,7 +3064,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3024
3064
 
3025
3065
  .lb-mix-match__slot--empty:focus-visible {
3026
3066
  outline: 2px solid var(--lb-primary-color);
3027
- outline-offset: 2px;
3067
+ outline-offset: -2px;
3068
+ box-shadow: none;
3028
3069
  border-radius: 8px;
3029
3070
  }
3030
3071
 
@@ -3091,6 +3132,13 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3091
3132
  text-decoration: underline;
3092
3133
  }
3093
3134
 
3135
+ .lb-mix-match__slot--filled a.lb-mix-match__filled-title:focus-visible {
3136
+ outline: 2px solid var(--lb-primary-color);
3137
+ outline-offset: 2px;
3138
+ box-shadow: none;
3139
+ border-radius: 2px;
3140
+ }
3141
+
3094
3142
  /* .lb-mix-match__filled-variant \u2014 styled jointly with
3095
3143
  .lb-bundle-variant-badge above to keep the variant text consistent
3096
3144
  across mix-match and fixed bundle widgets. */
@@ -3136,7 +3184,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3136
3184
 
3137
3185
  .lb-mix-match__slot-remove:focus-visible {
3138
3186
  outline: 2px solid var(--lb-primary-color);
3139
- outline-offset: 2px;
3187
+ outline-offset: -2px;
3188
+ box-shadow: none;
3140
3189
  }
3141
3190
 
3142
3191
  /* === Modal Overlay === */
@@ -3216,7 +3265,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3216
3265
 
3217
3266
  .lb-mix-match__modal-close:focus-visible {
3218
3267
  outline: 2px solid var(--lb-primary-color);
3219
- outline-offset: 2px;
3268
+ outline-offset: 0;
3269
+ box-shadow: none;
3220
3270
  }
3221
3271
 
3222
3272
  /* === Modal Search === */
@@ -3244,9 +3294,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3244
3294
  color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);
3245
3295
  }
3246
3296
 
3247
- .lb-mix-match__modal-search-input:focus {
3248
- outline: none;
3249
- box-shadow: 0 0 0 1px var(--lb-primary-color);
3297
+ .lb-mix-match__modal-search-input:focus-visible {
3298
+ outline: 2px solid var(--lb-primary-color);
3299
+ outline-offset: 2px;
3300
+ box-shadow: none;
3250
3301
  }
3251
3302
 
3252
3303
  .lb-mix-match__modal-search-clear {
@@ -3379,6 +3430,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3379
3430
  .lb-mix-match__variant-select:focus-visible {
3380
3431
  outline: 2px solid var(--lb-primary-color);
3381
3432
  outline-offset: 2px;
3433
+ box-shadow: none;
3382
3434
  }
3383
3435
 
3384
3436
  /* Per-pick quantity stepper inside the picker modal product row.
@@ -3433,13 +3485,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3433
3485
  justify-content: center;
3434
3486
  }
3435
3487
 
3436
- .lb-mix-match__qty-stepper-button:hover:not(:disabled) {
3437
- background: color-mix(in srgb, var(--lb-picker-text) 6%, transparent);
3438
- }
3439
-
3440
3488
  .lb-mix-match__qty-stepper-button:focus-visible {
3441
3489
  outline: 2px solid var(--lb-primary-color);
3442
3490
  outline-offset: -2px;
3491
+ box-shadow: none;
3443
3492
  }
3444
3493
 
3445
3494
  .lb-mix-match__qty-stepper-button:disabled {
@@ -3495,6 +3544,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3495
3544
  .lb-mix-match__modal-add:focus-visible {
3496
3545
  outline: 2px solid var(--lb-primary-color);
3497
3546
  outline-offset: 2px;
3547
+ box-shadow: none;
3498
3548
  }
3499
3549
 
3500
3550
  .lb-mix-match__modal-add:disabled {
@@ -3612,12 +3662,14 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
3612
3662
  border-color: var(--lb-tier-selected-border-color);
3613
3663
  }
3614
3664
 
3615
- /* Suppress the UA default focus outline so a freshly-clicked selected
3616
- tier doesn't briefly show the 1px focus ring on top of (or in place of)
3617
- the custom selected-state outline below. Keyboard focus is still
3618
- indicated by the :focus-visible rule. */
3665
+ /* Suppress the UA default focus outline + Dawn's focus shadow so a
3666
+ freshly-clicked selected tier doesn't briefly show the 1px focus ring
3667
+ or shadow on top of (or in place of) the custom selected-state
3668
+ outline below. Keyboard focus is still indicated by the
3669
+ :focus-visible rule. */
3619
3670
  .lb-volume__tier:focus {
3620
3671
  outline: none;
3672
+ box-shadow: none;
3621
3673
  }
3622
3674
 
3623
3675
  /* Keyboard focus indicator \u2014 explicitly excludes the selected tier so
@@ -3625,7 +3677,8 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
3625
3677
  outline property when both states apply at once. */
3626
3678
  .lb-volume__tier:focus-visible:not([aria-checked="true"]) {
3627
3679
  outline: 2px solid var(--lb-primary-color);
3628
- outline-offset: 2px;
3680
+ outline-offset: -2px;
3681
+ box-shadow: none;
3629
3682
  }
3630
3683
 
3631
3684
  .lb-volume__tier[aria-checked="true"] {
@@ -3773,6 +3826,7 @@ var BUNDLE_DROPDOWN_CSS = `/**
3773
3826
  .lb-dropdown-trigger:focus-visible {
3774
3827
  outline: 2px solid var(--lb-primary-color);
3775
3828
  outline-offset: 2px;
3829
+ box-shadow: none;
3776
3830
  }
3777
3831
 
3778
3832
  .lb-dropdown-trigger[aria-expanded="true"] {
@@ -4085,8 +4139,19 @@ var LimeBundleElement = class extends HTMLElement {
4085
4139
  "product-handle",
4086
4140
  "app-url",
4087
4141
  "analytics",
4088
- "locale"
4142
+ "locale",
4143
+ "country",
4144
+ "language",
4145
+ "market-id"
4089
4146
  ];
4147
+ /**
4148
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
4149
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
4150
+ * because the customer access token would land in DOM snapshots (Sentry,
4151
+ * analytics, browser extensions) and Referer headers. See the package
4152
+ * README ("Markets & B2B").
4153
+ */
4154
+ buyer = void 0;
4090
4155
  shadow;
4091
4156
  bundles = [];
4092
4157
  abortController = null;
@@ -4149,6 +4214,40 @@ var LimeBundleElement = class extends HTMLElement {
4149
4214
  get analyticsEnabled() {
4150
4215
  return this.getAttribute("analytics") !== "false";
4151
4216
  }
4217
+ get country() {
4218
+ return this.getAttribute("country") ?? void 0;
4219
+ }
4220
+ get language() {
4221
+ return this.getAttribute("language") ?? void 0;
4222
+ }
4223
+ get marketId() {
4224
+ return this.getAttribute("market-id") ?? void 0;
4225
+ }
4226
+ /**
4227
+ * Returns the @inContext-wrapped query when any context field is set;
4228
+ * otherwise the plain query. Keeps responses publicly cacheable when
4229
+ * no buyer/country/language is set.
4230
+ */
4231
+ wrapQueryForContext(query) {
4232
+ return hasInContext({
4233
+ shopDomain: this.shopDomain,
4234
+ accessToken: this.storefrontToken,
4235
+ country: this.country,
4236
+ language: this.language,
4237
+ buyer: this.buyer
4238
+ }) ? withInContext(query) : query;
4239
+ }
4240
+ /**
4241
+ * Returns true if this bundle should be hidden for the current market.
4242
+ * For "all" bundles, always returns false (visible). For "specific"
4243
+ * bundles, returns true when no marketId was set or when the bundle's
4244
+ * marketIds doesn't include the configured market.
4245
+ */
4246
+ isMarketHidden(bundle) {
4247
+ if (bundle.marketVisibility !== "specific") return false;
4248
+ if (!this.marketId) return true;
4249
+ return !bundle.marketIds.includes(this.marketId);
4250
+ }
4152
4251
  async fetchBundle() {
4153
4252
  if (!this.shopDomain || !this.storefrontToken) {
4154
4253
  this.renderError(
@@ -4162,7 +4261,10 @@ var LimeBundleElement = class extends HTMLElement {
4162
4261
  this.renderLoading();
4163
4262
  const client = createStorefrontClient({
4164
4263
  shopDomain: this.shopDomain,
4165
- accessToken: this.storefrontToken
4264
+ accessToken: this.storefrontToken,
4265
+ country: this.country,
4266
+ language: this.language,
4267
+ buyer: this.buyer
4166
4268
  });
4167
4269
  try {
4168
4270
  let bundlePromise;
@@ -4246,7 +4348,7 @@ var LimeBundleElement = class extends HTMLElement {
4246
4348
  }
4247
4349
  async fetchSingleBundle(client, signal) {
4248
4350
  const data = await client.query(
4249
- BUNDLE_METAOBJECT_QUERY,
4351
+ this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),
4250
4352
  { id: this.bundleGid },
4251
4353
  { signal }
4252
4354
  );
@@ -4258,11 +4360,11 @@ var LimeBundleElement = class extends HTMLElement {
4258
4360
  data.metaobject.id,
4259
4361
  data.metaobject.fields
4260
4362
  );
4261
- this.bundles = parsed ? [parsed] : [];
4363
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
4262
4364
  }
4263
4365
  async fetchProductBundles(client, signal, productHandle) {
4264
4366
  const data = await client.query(
4265
- BUNDLES_FOR_PRODUCT_QUERY,
4367
+ this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),
4266
4368
  { handle: productHandle },
4267
4369
  { signal }
4268
4370
  );
@@ -4274,7 +4376,7 @@ var LimeBundleElement = class extends HTMLElement {
4274
4376
  const bundles = [];
4275
4377
  for (const ref of refs) {
4276
4378
  const parsed = parseMetaobjectBundle(ref.id, ref.fields);
4277
- if (parsed) bundles.push(parsed);
4379
+ if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
4278
4380
  }
4279
4381
  this.bundles = bundles;
4280
4382
  }