@vio-live/web-sdk 0.21.0 → 0.23.1

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.
@@ -1,4 +1,4 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, ensureVippsButton, formatPrice, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, AdyenOriginError } from './chunk-WTVGVDZX.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, ensureVippsButton, formatPrice, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, normalizeScope } from './chunk-BRJMT7P4.js';
2
2
  import { LitElement, css, html } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -165,6 +165,141 @@ function applyVioTheme(overrides, target = typeof document !== "undefined" ? doc
165
165
  }
166
166
  }
167
167
  }
168
+
169
+ // src/ui/slot-registration.ts
170
+ var SCOPE_ATTR = "vio-scope";
171
+ var SCOPE_NAME_ATTR = "vio-scope-name";
172
+ var SCOPE_DATA_ATTR = "data-vio-scope";
173
+ var SCOPE_NAME_DATA_ATTR = "data-vio-scope-name";
174
+ var liveAutoIds = (() => {
175
+ const g = globalThis;
176
+ return g.__VIO_LIVE_SLOT_IDS__ ?? (g.__VIO_LIVE_SLOT_IDS__ = /* @__PURE__ */ new Map());
177
+ })();
178
+ var SlotRegistration = class {
179
+ constructor(host, kind) {
180
+ this.host = host;
181
+ this.kind = kind;
182
+ this.identity = null;
183
+ this.release = null;
184
+ /** The auto identity held (its number is kept while the element lives). */
185
+ this.auto = null;
186
+ /** Set by connect(); the scope label is read then too. */
187
+ this.connected = false;
188
+ this.scope = "";
189
+ this.scopeName = "";
190
+ }
191
+ /** The effective slot id ('' before connect / after disconnect). */
192
+ get effective() {
193
+ return this.identity?.id ?? "";
194
+ }
195
+ /** On connect: read the scope from the DOM, then register. */
196
+ connect(attrs) {
197
+ this.connected = true;
198
+ this.scope = scopeOf(this.host, attrs);
199
+ this.scopeName = scopeNameOf(this.host, attrs);
200
+ this.sync(attrs);
201
+ }
202
+ /** (Re)register when the attributes changed the identity. */
203
+ sync(attrs) {
204
+ if (!this.connected) return;
205
+ const scope = scopeOf(this.host, attrs);
206
+ if (scope !== this.scope) {
207
+ this.scope = scope;
208
+ this.scopeName = scopeNameOf(this.host, attrs);
209
+ this.releaseAuto();
210
+ } else {
211
+ const scopeName = scopeNameOf(this.host, attrs);
212
+ if (scopeName !== this.scopeName) this.scopeName = scopeName;
213
+ }
214
+ const next = this.resolve(attrs);
215
+ if (sameIdentity(next, this.identity)) return;
216
+ this.unregister();
217
+ if (!next) return;
218
+ try {
219
+ this.release = typeof Vio.registerSlot === "function" ? Vio.registerSlot(next.id, next.displayName) : null;
220
+ this.identity = next;
221
+ } catch {
222
+ this.release = null;
223
+ this.identity = null;
224
+ }
225
+ }
226
+ /** What the identity is for these attributes (null before connect without a host id). */
227
+ resolve(attrs) {
228
+ const explicit = normalizeSlotId(attrs.vioSlot);
229
+ const name = typeof attrs.vioSlotName === "string" && attrs.vioSlotName.trim() ? attrs.vioSlotName.trim() : void 0;
230
+ if (explicit) {
231
+ this.releaseAuto();
232
+ return { id: explicit, displayName: name };
233
+ }
234
+ if (!this.connected) return null;
235
+ const auto = this.claimAuto();
236
+ return { id: auto.id, displayName: name ?? autoSlotDisplayName(this.kind, auto.number - 1, this.scopeName) };
237
+ }
238
+ /** Unregister whatever is registered and free the auto number. */
239
+ clear() {
240
+ this.unregister();
241
+ this.releaseAuto();
242
+ this.connected = false;
243
+ }
244
+ unregister() {
245
+ try {
246
+ this.release?.();
247
+ } catch {
248
+ }
249
+ this.release = null;
250
+ this.identity = null;
251
+ }
252
+ /** The element's auto id: the one it already holds, else the first free number. */
253
+ claimAuto() {
254
+ if (this.auto && this.auto.scope === this.scope) return this.auto;
255
+ this.releaseAuto();
256
+ for (let number = 1; ; number++) {
257
+ const id = autoSlotId(this.kind, number - 1, this.scope);
258
+ const holders = liveAutoIds.get(id);
259
+ if (!holders || holders.size === 0 || holders.size === 1 && holders.has(this.host)) {
260
+ const set = holders ?? /* @__PURE__ */ new Set();
261
+ set.add(this.host);
262
+ liveAutoIds.set(id, set);
263
+ this.auto = { id, scope: this.scope, number };
264
+ return this.auto;
265
+ }
266
+ }
267
+ }
268
+ releaseAuto() {
269
+ if (!this.auto) return;
270
+ const holders = liveAutoIds.get(this.auto.id);
271
+ holders?.delete(this.host);
272
+ if (holders && holders.size === 0) liveAutoIds.delete(this.auto.id);
273
+ this.auto = null;
274
+ }
275
+ };
276
+ function sameIdentity(a, b) {
277
+ if (!a || !b) return a === b;
278
+ return a.id === b.id && a.displayName === b.displayName;
279
+ }
280
+ function scopeOf(el, attrs) {
281
+ const own = attrs && typeof attrs.vioScope === "string" ? attrs.vioScope : typeof el.vioScope === "string" ? el.vioScope ?? "" : el.getAttribute(SCOPE_ATTR) ?? "";
282
+ return normalizeScope(own) || normalizeScope(closestAcrossShadow(el, `[${SCOPE_DATA_ATTR}]`)?.getAttribute(SCOPE_DATA_ATTR));
283
+ }
284
+ function scopeNameOf(el, attrs) {
285
+ const own = attrs && typeof attrs.vioScopeName === "string" ? attrs.vioScopeName : typeof el.vioScopeName === "string" ? el.vioScopeName ?? "" : el.getAttribute(SCOPE_NAME_ATTR) ?? "";
286
+ return own.trim() || (closestAcrossShadow(el, `[${SCOPE_NAME_DATA_ATTR}]`)?.getAttribute(SCOPE_NAME_DATA_ATTR) ?? "").trim();
287
+ }
288
+ function closestAcrossShadow(start, selector) {
289
+ try {
290
+ let node = start;
291
+ while (node) {
292
+ const found = typeof node.closest === "function" ? node.closest(selector) : null;
293
+ if (found) return found;
294
+ const root = node.getRootNode();
295
+ node = root && root !== node && typeof root.host === "object" ? root.host ?? null : null;
296
+ }
297
+ } catch {
298
+ }
299
+ return null;
300
+ }
301
+
302
+ // src/ui/components/vio-product.ts
168
303
  var VioProduct = class extends LitElement {
169
304
  constructor() {
170
305
  super(...arguments);
@@ -175,6 +310,13 @@ var VioProduct = class extends LitElement {
175
310
  this.imageUrl = "";
176
311
  this.productId = "";
177
312
  this.sponsorId = "";
313
+ this.vioSlot = "";
314
+ this.vioSlotName = "";
315
+ this.vioScope = "";
316
+ this.vioScopeName = "";
317
+ this.vioContentId = "";
318
+ this.vioContentTitle = "";
319
+ this.slotRegistration = new SlotRegistration(this, "card");
178
320
  this.hasVariants = false;
179
321
  this.variants = void 0;
180
322
  this.options = void 0;
@@ -184,6 +326,36 @@ var VioProduct = class extends LitElement {
184
326
  this.justAdded = false;
185
327
  this.boundCartChange = () => this.updateInCart();
186
328
  }
329
+ /** The slot id in effect: `vio-slot`, else the auto id (`card-N`). Read-only. */
330
+ get effectiveSlot() {
331
+ return this.slotRegistration.effective;
332
+ }
333
+ /** The slot campaign's sponsor as the string the attributes use ('' when unknown). */
334
+ sponsorFromAttribution() {
335
+ const id = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot).sponsorId : void 0;
336
+ return id !== void 0 ? String(id) : "";
337
+ }
338
+ slotAttributes() {
339
+ return {
340
+ vioSlot: this.vioSlot,
341
+ vioSlotName: this.vioSlotName,
342
+ vioScope: this.vioScope,
343
+ vioScopeName: this.vioScopeName
344
+ };
345
+ }
346
+ /**
347
+ * What every event carries for attribution (`Vio.attribution`): the place,
348
+ * its campaign and sponsor, and the content — overridden here when the host
349
+ * set `vio-content-id` / `vio-content-title` on this card.
350
+ */
351
+ attribution() {
352
+ const base = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot, { sponsorId: this.sponsorId || void 0 }) : { slot: this.effectiveSlot || void 0 };
353
+ return {
354
+ ...base,
355
+ ...this.vioContentId ? { contentId: this.vioContentId } : {},
356
+ ...this.vioContentTitle ? { contentTitle: this.vioContentTitle } : {}
357
+ };
358
+ }
187
359
  static {
188
360
  this.styles = css`
189
361
  :host {
@@ -297,14 +469,19 @@ var VioProduct = class extends LitElement {
297
469
  connectedCallback() {
298
470
  super.connectedCallback();
299
471
  this.updateInCart();
472
+ this.slotRegistration.connect(this.slotAttributes());
300
473
  Vio.cart.addEventListener("change", this.boundCartChange);
301
474
  }
302
475
  disconnectedCallback() {
303
476
  Vio.cart.removeEventListener("change", this.boundCartChange);
477
+ this.slotRegistration.clear();
304
478
  super.disconnectedCallback();
305
479
  }
306
480
  updated(changed) {
307
481
  if (changed.has("productId") || changed.has("sponsorId")) this.updateInCart();
482
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
483
+ this.slotRegistration.sync(this.slotAttributes());
484
+ }
308
485
  }
309
486
  /** Reflect whether (and how many units of) this product sit in the cart. */
310
487
  updateInCart() {
@@ -330,12 +507,14 @@ var VioProduct = class extends LitElement {
330
507
  composed: true,
331
508
  detail: {
332
509
  productId: this.productId,
333
- sponsorId: this.sponsorId,
334
510
  brand: this.brand,
335
511
  name: this.name,
336
512
  price: this.price,
337
513
  retailer: this.retailer,
338
- imageUrl: this.imageUrl
514
+ imageUrl: this.imageUrl,
515
+ ...this.attribution(),
516
+ // The host's sponsor-id wins; without one, the slot's campaign sponsor.
517
+ sponsorId: this.sponsorId || this.sponsorFromAttribution()
339
518
  }
340
519
  })
341
520
  );
@@ -355,18 +534,28 @@ var VioProduct = class extends LitElement {
355
534
  this.onClick();
356
535
  return;
357
536
  }
537
+ const attribution = this.attribution();
358
538
  this.dispatchEvent(
359
539
  new CustomEvent("vio:product-add", {
360
540
  bubbles: true,
361
541
  composed: true,
362
542
  detail: {
363
543
  productId: this.productId,
364
- sponsorId: this.sponsorId,
365
544
  brand: this.brand,
366
545
  name: this.name,
367
546
  price: this.price,
368
547
  retailer: this.retailer,
369
- imageUrl: this.imageUrl
548
+ imageUrl: this.imageUrl,
549
+ ...attribution,
550
+ // The host's sponsor-id wins; without one, the slot's campaign sponsor.
551
+ sponsorId: this.sponsorId || this.sponsorFromAttribution(),
552
+ // The host adds to the cart: pass this through as `origin` so the
553
+ // purchase is attributed to this place and campaign.
554
+ origin: {
555
+ campaignId: attribution.campaignId,
556
+ slot: attribution.slot,
557
+ contentId: attribution.contentId
558
+ }
370
559
  }
371
560
  })
372
561
  );
@@ -434,6 +623,24 @@ __decorateClass([
434
623
  __decorateClass([
435
624
  property({ type: String, attribute: "sponsor-id" })
436
625
  ], VioProduct.prototype, "sponsorId", 2);
626
+ __decorateClass([
627
+ property({ type: String, attribute: "vio-slot" })
628
+ ], VioProduct.prototype, "vioSlot", 2);
629
+ __decorateClass([
630
+ property({ type: String, attribute: "vio-slot-name" })
631
+ ], VioProduct.prototype, "vioSlotName", 2);
632
+ __decorateClass([
633
+ property({ type: String, attribute: "vio-scope" })
634
+ ], VioProduct.prototype, "vioScope", 2);
635
+ __decorateClass([
636
+ property({ type: String, attribute: "vio-scope-name" })
637
+ ], VioProduct.prototype, "vioScopeName", 2);
638
+ __decorateClass([
639
+ property({ type: String, attribute: "vio-content-id" })
640
+ ], VioProduct.prototype, "vioContentId", 2);
641
+ __decorateClass([
642
+ property({ type: String, attribute: "vio-content-title" })
643
+ ], VioProduct.prototype, "vioContentTitle", 2);
437
644
  __decorateClass([
438
645
  property({ type: Boolean, attribute: "has-variants" })
439
646
  ], VioProduct.prototype, "hasVariants", 2);
@@ -468,6 +675,13 @@ var VioProductCarousel = class extends LitElement {
468
675
  this.sponsorId = 0;
469
676
  this.productRefs = "";
470
677
  this.currency = "NOK";
678
+ this.vioSlot = "";
679
+ this.vioSlotName = "";
680
+ this.vioScope = "";
681
+ this.vioScopeName = "";
682
+ this.vioContentId = "";
683
+ this.vioContentTitle = "";
684
+ this.slotRegistration = new SlotRegistration(this, "carousel");
471
685
  this.fetched = null;
472
686
  this.isLoading = false;
473
687
  this.fetchError = "";
@@ -479,6 +693,27 @@ var VioProductCarousel = class extends LitElement {
479
693
  this.impressed = false;
480
694
  this.listTracked = false;
481
695
  }
696
+ /** The slot id in effect: `vio-slot`, else the auto id (`carousel-N`). Read-only. */
697
+ get effectiveSlot() {
698
+ return this.slotRegistration.effective;
699
+ }
700
+ slotAttributes() {
701
+ return {
702
+ vioSlot: this.vioSlot,
703
+ vioSlotName: this.vioSlotName,
704
+ vioScope: this.vioScope,
705
+ vioScopeName: this.vioScopeName
706
+ };
707
+ }
708
+ /** Attribution every analytics event of this carousel carries (`Vio.attribution` + the content overrides). */
709
+ attribution() {
710
+ const base = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot, { sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0 }) : { slot: this.effectiveSlot || void 0 };
711
+ return {
712
+ ...base,
713
+ ...this.vioContentId ? { contentId: this.vioContentId } : {},
714
+ ...this.vioContentTitle ? { contentTitle: this.vioContentTitle } : {}
715
+ };
716
+ }
482
717
  static {
483
718
  this.styles = css`
484
719
  :host {
@@ -626,6 +861,7 @@ var VioProductCarousel = class extends LitElement {
626
861
  }
627
862
  connectedCallback() {
628
863
  super.connectedCallback();
864
+ this.slotRegistration.connect(this.slotAttributes());
629
865
  this.observeForAnalytics();
630
866
  if (this.productRefs.trim() || this.productIds.trim()) {
631
867
  void this.autoFetch();
@@ -634,14 +870,28 @@ var VioProductCarousel = class extends LitElement {
634
870
  disconnectedCallback() {
635
871
  this.unobserveImpression?.();
636
872
  this.unobserveImpression = null;
873
+ this.slotRegistration.clear();
637
874
  super.disconnectedCallback();
638
875
  }
639
876
  updated(changed) {
640
- const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
877
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
878
+ this.slotRegistration.sync(this.slotAttributes());
879
+ }
880
+ const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId") || changed.has("vioSlot");
641
881
  if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
642
882
  void this.autoFetch();
643
883
  }
644
884
  }
885
+ /**
886
+ * The sponsor of this carousel: the host's `sponsor-id`, else the primary
887
+ * sponsor of the campaign the slot shows (ADR-0034 — "the campaign of the
888
+ * block is its sponsor"), else 0 (unknown).
889
+ */
890
+ effectiveSponsorId() {
891
+ if (this.sponsorId > 0) return this.sponsorId;
892
+ const fromSlot = typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.effectiveSlot || null)?.primarySponsor?.id) : NaN;
893
+ return Number.isFinite(fromSlot) && fromSlot > 0 ? fromSlot : 0;
894
+ }
645
895
  async autoFetch() {
646
896
  if (!Vio.isInitialized) {
647
897
  this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
@@ -650,7 +900,14 @@ var VioProductCarousel = class extends LitElement {
650
900
  this.isLoading = true;
651
901
  this.fetchError = "";
652
902
  try {
653
- const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
903
+ if (this.productRefs.trim() === "" && !this.commerceKey && this.sponsorId <= 0 && !Vio.bootstrapCache) {
904
+ try {
905
+ await Vio.bootstrap();
906
+ } catch {
907
+ }
908
+ }
909
+ const sponsorId = this.effectiveSponsorId();
910
+ const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId, productId: id }));
654
911
  if (refs.length === 0) {
655
912
  this.fetched = [];
656
913
  return;
@@ -662,23 +919,23 @@ var VioProductCarousel = class extends LitElement {
662
919
  bySponsor.set(r.sponsorId, arr);
663
920
  }
664
921
  const fetched = await Promise.all(
665
- Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
666
- const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
922
+ Array.from(bySponsor.entries()).map(async ([sponsorId2, ids]) => {
923
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2);
667
924
  const products = await commerce.channel.product.getByIds({
668
925
  product_ids: ids,
669
926
  currency: this.currency,
670
927
  image_size: "large"
671
928
  });
672
- return { sponsorId, products };
929
+ return { sponsorId: sponsorId2, products };
673
930
  })
674
931
  );
675
932
  const lookup = /* @__PURE__ */ new Map();
676
- for (const { sponsorId, products } of fetched) {
677
- for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
933
+ for (const { sponsorId: sponsorId2, products } of fetched) {
934
+ for (const p of products) lookup.set(`${sponsorId2}:${p.id}`, p);
678
935
  }
679
- this.fetched = refs.map(({ sponsorId, productId }) => {
680
- const product = lookup.get(`${sponsorId}:${productId}`);
681
- return product ? { product, sponsorId } : null;
936
+ this.fetched = refs.map(({ sponsorId: sponsorId2, productId }) => {
937
+ const product = lookup.get(`${sponsorId2}:${productId}`);
938
+ return product ? { product, sponsorId: sponsorId2 } : null;
682
939
  }).filter((x) => x !== null);
683
940
  this.maybeTrackList();
684
941
  } catch (err) {
@@ -695,10 +952,12 @@ var VioProductCarousel = class extends LitElement {
695
952
  {
696
953
  sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0,
697
954
  componentTemplateId: "product-carousel",
698
- locationId: this.label || void 0
955
+ // The slot is the real location id; the label only stands in for it.
956
+ locationId: this.effectiveSlot || this.label || void 0,
957
+ ...this.attribution()
699
958
  },
700
959
  {
701
- key: `carousel:${this.label || this.productIds || this.productRefs || "default"}`,
960
+ key: `carousel:${this.effectiveSlot || this.label || this.productIds || this.productRefs || "default"}`,
702
961
  onImpression: () => {
703
962
  this.impressed = true;
704
963
  this.maybeTrackList();
@@ -716,14 +975,16 @@ var VioProductCarousel = class extends LitElement {
716
975
  if (entries.length === 0) return;
717
976
  this.listTracked = true;
718
977
  try {
978
+ const sponsorId = this.effectiveSponsorId();
719
979
  Vio.analytics.track("view_item_list", {
720
980
  context: {
721
- sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0,
981
+ sponsorId: sponsorId > 0 ? sponsorId : void 0,
722
982
  componentTemplateId: "product-carousel",
723
- locationId: this.label || void 0
983
+ locationId: this.effectiveSlot || this.label || void 0,
984
+ ...this.attribution()
724
985
  },
725
986
  commerce: {
726
- items: entries.map(({ product, sponsorId }) => ({
987
+ items: entries.map(({ product, sponsorId: sponsorId2 }) => ({
727
988
  productId: product.id,
728
989
  name: product.title,
729
990
  price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
@@ -776,6 +1037,9 @@ var VioProductCarousel = class extends LitElement {
776
1037
  retailer=${retailer}
777
1038
  image-url=${img}
778
1039
  ?has-variants=${(p.variants?.length ?? 0) > 1}
1040
+ .vioSlot=${this.effectiveSlot}
1041
+ .vioContentId=${this.vioContentId}
1042
+ .vioContentTitle=${this.vioContentTitle}
779
1043
  ></vio-product>
780
1044
  `;
781
1045
  })}`;
@@ -845,6 +1109,24 @@ __decorateClass([
845
1109
  __decorateClass([
846
1110
  property({ type: String })
847
1111
  ], VioProductCarousel.prototype, "currency", 2);
1112
+ __decorateClass([
1113
+ property({ type: String, attribute: "vio-slot" })
1114
+ ], VioProductCarousel.prototype, "vioSlot", 2);
1115
+ __decorateClass([
1116
+ property({ type: String, attribute: "vio-slot-name" })
1117
+ ], VioProductCarousel.prototype, "vioSlotName", 2);
1118
+ __decorateClass([
1119
+ property({ type: String, attribute: "vio-scope" })
1120
+ ], VioProductCarousel.prototype, "vioScope", 2);
1121
+ __decorateClass([
1122
+ property({ type: String, attribute: "vio-scope-name" })
1123
+ ], VioProductCarousel.prototype, "vioScopeName", 2);
1124
+ __decorateClass([
1125
+ property({ type: String, attribute: "vio-content-id" })
1126
+ ], VioProductCarousel.prototype, "vioContentId", 2);
1127
+ __decorateClass([
1128
+ property({ type: String, attribute: "vio-content-title" })
1129
+ ], VioProductCarousel.prototype, "vioContentTitle", 2);
848
1130
  __decorateClass([
849
1131
  query(".scroller")
850
1132
  ], VioProductCarousel.prototype, "scroller", 2);
@@ -1083,6 +1365,18 @@ var VioProductDetail = class extends LitElement {
1083
1365
  this.sponsorId = 0;
1084
1366
  this.autoOpen = true;
1085
1367
  this.currency = "NOK";
1368
+ this.vioSlot = "";
1369
+ this.vioSlotName = "";
1370
+ this.vioScope = "";
1371
+ this.vioScopeName = "";
1372
+ this.vioContentId = "";
1373
+ this.vioContentTitle = "";
1374
+ this.slotRegistration = new SlotRegistration(this, "detail");
1375
+ /** What the click that opened the detail carried ('' when nothing), and for which product. */
1376
+ this.eventSlot = "";
1377
+ this.eventContentId = "";
1378
+ this.eventContentTitle = "";
1379
+ this.eventProductId = "";
1086
1380
  this.product = null;
1087
1381
  this.sellerName = null;
1088
1382
  this.sellerSettled = false;
@@ -1113,8 +1407,17 @@ var VioProductDetail = class extends LitElement {
1113
1407
  const ev = e;
1114
1408
  const productIdNum = parseInt(ev.detail.productId, 10);
1115
1409
  if (Number.isNaN(productIdNum)) return;
1116
- const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
1117
- if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
1410
+ const slot = typeof ev.detail.slot === "string" ? ev.detail.slot.trim() : "";
1411
+ let sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
1412
+ if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) {
1413
+ const fromSlot = typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(slot || this.effectiveSlot || null)?.primarySponsor?.id) : NaN;
1414
+ if (!Number.isFinite(fromSlot) || fromSlot <= 0) return;
1415
+ sponsorIdNum = fromSlot;
1416
+ }
1417
+ this.eventSlot = slot;
1418
+ this.eventProductId = String(ev.detail.productId);
1419
+ this.eventContentId = ev.detail.contentId !== void 0 && ev.detail.contentId !== null ? String(ev.detail.contentId).trim() : "";
1420
+ this.eventContentTitle = typeof ev.detail.contentTitle === "string" ? ev.detail.contentTitle.trim() : "";
1118
1421
  this.productId = ev.detail.productId;
1119
1422
  this.sponsorId = sponsorIdNum;
1120
1423
  this.currency = ev.detail.currency || getGlobalCurrency();
@@ -1122,6 +1425,37 @@ var VioProductDetail = class extends LitElement {
1122
1425
  void this.fetchProduct();
1123
1426
  };
1124
1427
  }
1428
+ /** The card's context applies to the product it opened — not to one the host opened by hand later. */
1429
+ get fromCard() {
1430
+ return this.eventProductId !== "" && this.eventProductId === String(this.productId);
1431
+ }
1432
+ /** The slot id in effect for this element: `vio-slot`, else the auto id (`detail-N`). Read-only. */
1433
+ get effectiveSlot() {
1434
+ return this.slotRegistration.effective;
1435
+ }
1436
+ /** The slot the open product is attributed to: the opening card's, else this element's. */
1437
+ get activeSlot() {
1438
+ return this.fromCard && this.eventSlot || this.effectiveSlot;
1439
+ }
1440
+ slotAttributes() {
1441
+ return {
1442
+ vioSlot: this.vioSlot,
1443
+ vioSlotName: this.vioSlotName,
1444
+ vioScope: this.vioScope,
1445
+ vioScopeName: this.vioScopeName
1446
+ };
1447
+ }
1448
+ /** Attribution every event of the open product carries (`Vio.attribution` of the opening card's slot, else this element's). */
1449
+ attribution() {
1450
+ const base = typeof Vio.attribution === "function" ? Vio.attribution(this.activeSlot, { sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0 }) : { slot: this.activeSlot || void 0 };
1451
+ const contentId = this.fromCard && this.eventContentId || this.vioContentId;
1452
+ const contentTitle = this.fromCard && this.eventContentTitle || this.vioContentTitle;
1453
+ return {
1454
+ ...base,
1455
+ ...contentId ? { contentId } : {},
1456
+ ...contentTitle ? { contentTitle } : {}
1457
+ };
1458
+ }
1125
1459
  static {
1126
1460
  this.styles = css`
1127
1461
  :host { display: contents; }
@@ -1569,15 +1903,20 @@ var VioProductDetail = class extends LitElement {
1569
1903
  }
1570
1904
  connectedCallback() {
1571
1905
  super.connectedCallback();
1906
+ this.slotRegistration.connect(this.slotAttributes());
1572
1907
  void this.loadVippsButton();
1573
1908
  document.addEventListener("vio:product-click", this.boundProductClick);
1574
1909
  }
1575
1910
  disconnectedCallback() {
1576
1911
  document.removeEventListener("vio:product-click", this.boundProductClick);
1912
+ this.slotRegistration.clear();
1577
1913
  super.disconnectedCallback();
1578
1914
  }
1579
1915
  updated(changed) {
1580
1916
  if (changed.has("open")) this.dialogFocus.update(this.open);
1917
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
1918
+ this.slotRegistration.sync(this.slotAttributes());
1919
+ }
1581
1920
  }
1582
1921
  show() {
1583
1922
  this.open = true;
@@ -1586,6 +1925,10 @@ var VioProductDetail = class extends LitElement {
1586
1925
  this.open = false;
1587
1926
  setTimeout(() => {
1588
1927
  if (!this.open) {
1928
+ this.eventSlot = "";
1929
+ this.eventContentId = "";
1930
+ this.eventContentTitle = "";
1931
+ this.eventProductId = "";
1589
1932
  this.product = null;
1590
1933
  this.sellerName = null;
1591
1934
  this.sellerSettled = false;
@@ -1644,7 +1987,8 @@ var VioProductDetail = class extends LitElement {
1644
1987
  Vio.analytics.track("view_item", {
1645
1988
  context: {
1646
1989
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
1647
- componentTemplateId: "product-detail"
1990
+ componentTemplateId: "product-detail",
1991
+ ...this.attribution()
1648
1992
  },
1649
1993
  commerce: {
1650
1994
  items: [
@@ -1684,7 +2028,7 @@ var VioProductDetail = class extends LitElement {
1684
2028
  }
1685
2029
  /** Which buy buttons to render — backend-configured per sponsor. */
1686
2030
  async loadAvailablePaymentMethods() {
1687
- const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
2031
+ const spId = this.sponsorId || (typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.activeSlot || null)?.primarySponsor?.id) : 0) || globalThis.__VIO_SPONSOR_ID__;
1688
2032
  if (!spId) return;
1689
2033
  try {
1690
2034
  const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
@@ -1966,12 +2310,14 @@ var VioProductDetail = class extends LitElement {
1966
2310
  const sponsor = Vio.bootstrapCache?.primarySponsor;
1967
2311
  const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
1968
2312
  const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
2313
+ const { campaignId, slot, contentId } = this.attribution();
1969
2314
  Vio.cart.addProduct({
1970
2315
  product: this.product,
1971
2316
  sponsorId: this.sponsorId,
1972
2317
  sponsorName,
1973
2318
  quantity: this.quantity,
1974
- variantId: variant?.id
2319
+ variantId: variant?.id,
2320
+ origin: { campaignId, slot, contentId }
1975
2321
  });
1976
2322
  return variant?.id;
1977
2323
  }
@@ -1988,7 +2334,8 @@ var VioProductDetail = class extends LitElement {
1988
2334
  productId: this.product.id,
1989
2335
  sponsorId: this.sponsorId,
1990
2336
  quantity: this.quantity,
1991
- variantId
2337
+ variantId,
2338
+ ...this.attribution()
1992
2339
  }
1993
2340
  })
1994
2341
  );
@@ -2013,7 +2360,7 @@ var VioProductDetail = class extends LitElement {
2013
2360
  new CustomEvent("vio:checkout-open", {
2014
2361
  bubbles: true,
2015
2362
  composed: true,
2016
- detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
2363
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true, ...this.attribution() }
2017
2364
  })
2018
2365
  );
2019
2366
  } finally {
@@ -2043,7 +2390,7 @@ var VioProductDetail = class extends LitElement {
2043
2390
  new CustomEvent("vio:checkout-open", {
2044
2391
  bubbles: true,
2045
2392
  composed: true,
2046
- detail: { sponsorId: this.sponsorId, express: false }
2393
+ detail: { sponsorId: this.sponsorId, express: false, ...this.attribution() }
2047
2394
  })
2048
2395
  );
2049
2396
  }
@@ -2079,7 +2426,7 @@ var VioProductDetail = class extends LitElement {
2079
2426
  new CustomEvent("vio:checkout-open", {
2080
2427
  bubbles: true,
2081
2428
  composed: true,
2082
- detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
2429
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false, ...this.attribution() }
2083
2430
  })
2084
2431
  );
2085
2432
  }
@@ -2092,7 +2439,7 @@ var VioProductDetail = class extends LitElement {
2092
2439
  new CustomEvent("vio:checkout-open", {
2093
2440
  bubbles: true,
2094
2441
  composed: true,
2095
- detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
2442
+ detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false, ...this.attribution() }
2096
2443
  })
2097
2444
  );
2098
2445
  }
@@ -2375,6 +2722,24 @@ __decorateClass([
2375
2722
  __decorateClass([
2376
2723
  property({ type: String })
2377
2724
  ], VioProductDetail.prototype, "currency", 2);
2725
+ __decorateClass([
2726
+ property({ type: String, attribute: "vio-slot" })
2727
+ ], VioProductDetail.prototype, "vioSlot", 2);
2728
+ __decorateClass([
2729
+ property({ type: String, attribute: "vio-slot-name" })
2730
+ ], VioProductDetail.prototype, "vioSlotName", 2);
2731
+ __decorateClass([
2732
+ property({ type: String, attribute: "vio-scope" })
2733
+ ], VioProductDetail.prototype, "vioScope", 2);
2734
+ __decorateClass([
2735
+ property({ type: String, attribute: "vio-scope-name" })
2736
+ ], VioProductDetail.prototype, "vioScopeName", 2);
2737
+ __decorateClass([
2738
+ property({ type: String, attribute: "vio-content-id" })
2739
+ ], VioProductDetail.prototype, "vioContentId", 2);
2740
+ __decorateClass([
2741
+ property({ type: String, attribute: "vio-content-title" })
2742
+ ], VioProductDetail.prototype, "vioContentTitle", 2);
2378
2743
  __decorateClass([
2379
2744
  state()
2380
2745
  ], VioProductDetail.prototype, "product", 2);
@@ -3001,6 +3366,7 @@ var VioCart = class extends LitElement {
3001
3366
  async startVippsExpressOrCheckout(sponsorId) {
3002
3367
  let fallback = true;
3003
3368
  try {
3369
+ await Vio.cart.waitForCartMutations(sponsorId);
3004
3370
  if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
3005
3371
  const res = await Vio.checkout.startVippsExpress(sponsorId);
3006
3372
  fallback = !res?.payment_url;
@@ -3042,11 +3408,14 @@ var VioCart = class extends LitElement {
3042
3408
  for (const cart of this.carts.values()) return cart.currency;
3043
3409
  return "NOK";
3044
3410
  }
3411
+ // A delta on the manager's live quantity, not a target computed from this
3412
+ // render's snapshot: two clicks before Lit re-renders used to ask for the
3413
+ // same quantity twice (Alan, 2026-10-15).
3045
3414
  onIncrement(item) {
3046
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
3415
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, 1);
3047
3416
  }
3048
3417
  onDecrement(item) {
3049
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
3418
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, -1);
3050
3419
  }
3051
3420
  onRemove(item) {
3052
3421
  Vio.cart.removeItem(item.id, item.sponsorId);
@@ -6665,5 +7034,5 @@ function registerVioElements() {
6665
7034
  }
6666
7035
 
6667
7036
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
6668
- //# sourceMappingURL=chunk-MRJR6DG5.js.map
6669
- //# sourceMappingURL=chunk-MRJR6DG5.js.map
7037
+ //# sourceMappingURL=chunk-UVDOKFGD.js.map
7038
+ //# sourceMappingURL=chunk-UVDOKFGD.js.map