@vio-live/web-sdk 0.33.0 → 0.36.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.
@@ -1,4 +1,4 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-ZORYG4W3.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, resolveDetailLayout, toCampaignId, getGlobalCurrency, copyAppliedLayout, onGlobalDetailLayoutChange, layoutSignature, layoutOriginOf, ensureVippsButton, formatPrice, currentGlobalDescriptionLabel, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-6PQFXUOW.js';
2
2
  import { LitElement, css, html, nothing } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -171,6 +171,7 @@ var THEME_KEY_TO_CSS_VAR = {
171
171
  detailDescriptionPreviewLines: "--vio-detail-description-preview-lines",
172
172
  detailHighlightsDisplay: "--vio-detail-highlights-display",
173
173
  detailShippingReturnsDisplay: "--vio-detail-shipping-returns-display",
174
+ cartHighlightsDisplay: "--vio-cart-highlights-display",
174
175
  detailThumbSize: "--vio-detail-thumb-size",
175
176
  detailThumbGap: "--vio-detail-thumb-gap",
176
177
  detailThumbRadius: "--vio-detail-thumb-radius",
@@ -201,7 +202,8 @@ var THEME_KEY_TO_CSS_VAR = {
201
202
  confirmTitleSize: "--vio-confirm-title-size",
202
203
  confirmCheckBg: "--vio-confirm-check-bg",
203
204
  redirectBg: "--vio-redirect-bg",
204
- redirectSpinner: "--vio-redirect-spinner"
205
+ redirectSpinner: "--vio-redirect-spinner",
206
+ vippsCartPayment: "--vio-vipps-cart-payment"
205
207
  };
206
208
  function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
207
209
  if (!target) return;
@@ -1556,6 +1558,21 @@ function returnCancelled(detail) {
1556
1558
  emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
1557
1559
  }
1558
1560
 
1561
+ // src/ui/vipps-cart-payment.ts
1562
+ var VIPPS_CART_PAYMENT_VAR = "--vio-vipps-cart-payment";
1563
+ function resolveVippsCartPayment(el, own) {
1564
+ let fromTheme = "";
1565
+ if (!own) {
1566
+ try {
1567
+ fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(el).getPropertyValue(VIPPS_CART_PAYMENT_VAR) : "";
1568
+ } catch {
1569
+ fromTheme = "";
1570
+ }
1571
+ }
1572
+ const value = String(own || fromTheme || "").trim().replace(/^["']|["']$/g, "").toLowerCase();
1573
+ return value === "checkout" ? "checkout" : "express";
1574
+ }
1575
+
1559
1576
  // src/core/checkout/method-taxonomy.ts
1560
1577
  function normalizeMethodName(name) {
1561
1578
  return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
@@ -2035,12 +2052,6 @@ function unlockPageScroll() {
2035
2052
  window.scrollTo(0, scrollY);
2036
2053
  root.style.scrollBehavior = behavior;
2037
2054
  }
2038
-
2039
- // src/ui/components/vio-product-detail.ts
2040
- var ADDED_TOAST_MS = 8e3;
2041
- var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
2042
- '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
2043
- );
2044
2055
  var HIGHLIGHT_ICONS = {
2045
2056
  plane: "M21.5 2.5 2.5 10l7 3m12-10.5-12 10.5m12-10.5L17 21l-7.5-8m0 0V20l3-4M5 17l-2.5 2.5M7.5 18.5 6 20",
2046
2057
  heart: "M12 20.5s-8.5-5-8.5-11A4.75 4.75 0 0 1 12 6.75 4.75 4.75 0 0 1 20.5 9.5c0 6-8.5 11-8.5 11Z",
@@ -2055,6 +2066,72 @@ var HIGHLIGHT_ICONS = {
2055
2066
  pin: "M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
2056
2067
  clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13.5V12l3 2"
2057
2068
  };
2069
+ function renderHighlights(items) {
2070
+ const list = (Array.isArray(items) ? items : []).filter((h) => h && h.title);
2071
+ if (list.length === 0) return nothing;
2072
+ return html`<ul class="highlights" part="highlights">
2073
+ ${list.map((h) => {
2074
+ const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
2075
+ const icon = path ? html`<svg class="hl-icon" viewBox="0 0 24 24" aria-hidden="true"><path d=${path}></path></svg>` : h.icon ? html`<img class="hl-icon" src=${h.icon} alt="" />` : nothing;
2076
+ return html`<li class="hl" part="highlight">
2077
+ ${icon}
2078
+ <span class="hl-title">${h.title}</span>
2079
+ ${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
2080
+ </li>`;
2081
+ })}
2082
+ </ul>`;
2083
+ }
2084
+ function highlightsStyles(scope) {
2085
+ const display = scope === "detail" ? css`var(--vio-detail-highlights-display, grid)` : css`var(--vio-cart-highlights-display, grid)`;
2086
+ const padding = scope === "detail" ? css`var(--vio-detail-highlights-padding, 16px 0 2px)` : css`var(--vio-cart-highlights-padding, 14px 0 2px)`;
2087
+ return css`
2088
+ /* Like a shop's trust strip: a thin icon, the promise in regular weight,
2089
+ a smaller line under it — nothing bold. */
2090
+ .highlights {
2091
+ list-style: none;
2092
+ margin: 0;
2093
+ padding: ${padding};
2094
+ display: ${display};
2095
+ grid-auto-flow: column;
2096
+ grid-auto-columns: 1fr;
2097
+ gap: 10px;
2098
+ border-top: 1px solid var(--vio-color-border, #e5e5e5);
2099
+ }
2100
+ .hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
2101
+ .hl-icon {
2102
+ width: var(--vio-detail-highlight-icon-size, 22px);
2103
+ height: var(--vio-detail-highlight-icon-size, 22px);
2104
+ margin-bottom: 8px;
2105
+ fill: none;
2106
+ stroke: currentColor;
2107
+ stroke-width: 1.2;
2108
+ stroke-linecap: round;
2109
+ stroke-linejoin: round;
2110
+ object-fit: contain;
2111
+ }
2112
+ .hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
2113
+ .hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
2114
+ `;
2115
+ }
2116
+
2117
+ // src/ui/components/vio-product-detail.ts
2118
+ var layoutAttribute = {
2119
+ fromAttribute(value) {
2120
+ if (value === null || value.trim() === "") return null;
2121
+ try {
2122
+ return JSON.parse(value);
2123
+ } catch {
2124
+ return value;
2125
+ }
2126
+ },
2127
+ toAttribute(value) {
2128
+ return value === null || value === void 0 ? null : JSON.stringify(value);
2129
+ }
2130
+ };
2131
+ var ADDED_TOAST_MS = 8e3;
2132
+ var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
2133
+ '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
2134
+ );
2058
2135
  function rateIcon(method) {
2059
2136
  const m = method.toLowerCase();
2060
2137
  if (/postkasse|mailbox|brevkasse/.test(m)) return "mailbox";
@@ -2087,11 +2164,19 @@ var VioProductDetail = class extends LitElement {
2087
2164
  this.vioContentId = "";
2088
2165
  this.vioContentTitle = "";
2089
2166
  this.noAddedToast = false;
2167
+ this.vippsCartPayment = "";
2090
2168
  this.gallery = "";
2091
2169
  this.descriptionMode = "";
2170
+ this.descriptionLabel = "";
2092
2171
  this.highlights = [];
2093
2172
  this.deliveryTime = "";
2094
2173
  this.carrierLogos = {};
2174
+ this.layout = null;
2175
+ /** The layout the detail renders with now (resolved before each render). */
2176
+ this.layoutNow = resolveDetailLayout(null);
2177
+ /** The product and layout `vio:detail-layout-applied` was last said for. */
2178
+ this.appliedFor = null;
2179
+ this.unsubscribeLayout = null;
2095
2180
  this.slotRegistration = new SlotRegistration(this, "detail");
2096
2181
  /** What the click that opened the detail carried ('' when nothing), and for which product. */
2097
2182
  this.eventSlot = "";
@@ -2180,6 +2265,10 @@ var VioProductDetail = class extends LitElement {
2180
2265
  this.show();
2181
2266
  void this.fetchProduct();
2182
2267
  };
2268
+ /** The cart changed (another tab, the drawer): the line under Vipps counts it again. */
2269
+ this.boundCartChange = () => {
2270
+ if (this.open) this.requestUpdate();
2271
+ };
2183
2272
  /** The photo in view is the active one (dots, Apple Pay sheet, events). */
2184
2273
  this.onCarouselScroll = () => {
2185
2274
  const track = this.carouselTrack();
@@ -2198,6 +2287,14 @@ var VioProductDetail = class extends LitElement {
2198
2287
  if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
2199
2288
  };
2200
2289
  }
2290
+ /**
2291
+ * The layout this detail applies (0.34): `{ id, revision, experimentId?,
2292
+ * variantId?, sections, adjusted, source }` — `source` is `element` (its own
2293
+ * `layout`), `global` (`Vio.configureDetail`) or `default`. A copy.
2294
+ */
2295
+ get appliedLayout() {
2296
+ return copyAppliedLayout(resolveDetailLayout(this.layout));
2297
+ }
2201
2298
  /** The card's context applies to the product it opened — not to one the host opened by hand later. */
2202
2299
  get fromCard() {
2203
2300
  return this.eventProductId !== "" && this.eventProductId === String(this.productId);
@@ -2259,7 +2356,8 @@ var VioProductDetail = class extends LitElement {
2259
2356
  };
2260
2357
  }
2261
2358
  static {
2262
- this.styles = css`
2359
+ this.styles = [
2360
+ css`
2263
2361
  :host { display: contents; }
2264
2362
 
2265
2363
  .backdrop {
@@ -2632,35 +2730,6 @@ var VioProductDetail = class extends LitElement {
2632
2730
  margin: 0;
2633
2731
  letter-spacing: -0.01em;
2634
2732
  }
2635
- /* The brand's promises: one row, an icon over a title and a line of text,
2636
- centred like a shop's own trust strip. */
2637
- .highlights {
2638
- list-style: none;
2639
- /* --vio-detail-highlights-display: none hides the strip (theme detailHighlightsDisplay). */
2640
- margin: 0;
2641
- padding: var(--vio-detail-highlights-padding, 16px 0 2px);
2642
- display: var(--vio-detail-highlights-display, grid);
2643
- grid-auto-flow: column;
2644
- grid-auto-columns: 1fr;
2645
- gap: 10px;
2646
- border-top: 1px solid var(--vio-color-border, #e5e5e5);
2647
- }
2648
- /* Like a shop's trust strip: a thin icon, the promise in regular weight,
2649
- a smaller line under it — nothing bold. */
2650
- .hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
2651
- .hl-icon {
2652
- width: var(--vio-detail-highlight-icon-size, 22px);
2653
- height: var(--vio-detail-highlight-icon-size, 22px);
2654
- margin-bottom: 8px;
2655
- fill: none;
2656
- stroke: currentColor;
2657
- stroke-width: 1.2;
2658
- stroke-linecap: round;
2659
- stroke-linejoin: round;
2660
- object-fit: contain;
2661
- }
2662
- .hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
2663
- .hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
2664
2733
  /* «Frakt & retur»: same fold as «Beskrivelse», with a small list of rates.
2665
2734
  --vio-detail-shipping-returns-display: none hides it (theme detailShippingReturnsDisplay). */
2666
2735
  .shipping-returns { display: var(--vio-detail-shipping-returns-display, block); }
@@ -2714,6 +2783,10 @@ var VioProductDetail = class extends LitElement {
2714
2783
  }
2715
2784
  /* The sizes follow the price closely: they are the next thing to do. */
2716
2785
  .headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
2786
+ /* A layout with the sizes apart from the buy zone (0.34): they follow the
2787
+ price closely instead, and the buy zone keeps the normal gap. */
2788
+ .headline + .buy-options.options-apart { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
2789
+ .headline + .purchase.options-apart { margin-top: 0; }
2717
2790
  .price {
2718
2791
  font-size: var(--vio-detail-price-size, 24px);
2719
2792
  font-weight: var(--vio-detail-price-weight, 700);
@@ -3201,10 +3274,15 @@ var VioProductDetail = class extends LitElement {
3201
3274
  color: var(--vio-color-accent, #c14a3b);
3202
3275
  font-size: 14px;
3203
3276
  }
3204
- `;
3277
+ `,
3278
+ highlightsStyles("detail")
3279
+ ];
3205
3280
  }
3206
3281
  connectedCallback() {
3207
3282
  super.connectedCallback();
3283
+ Vio.cart.addEventListener("change", this.boundCartChange);
3284
+ this.unsubscribeLayout?.();
3285
+ this.unsubscribeLayout = onGlobalDetailLayoutChange(() => this.requestUpdate());
3208
3286
  this.slotRegistration.connect(this.slotAttributes());
3209
3287
  void this.loadVippsButton();
3210
3288
  document.addEventListener("vio:product-click", this.boundProductClick);
@@ -3213,8 +3291,11 @@ var VioProductDetail = class extends LitElement {
3213
3291
  if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3214
3292
  this.addedToastTimer = null;
3215
3293
  document.removeEventListener("vio:product-click", this.boundProductClick);
3294
+ Vio.cart.removeEventListener("change", this.boundCartChange);
3216
3295
  this.slotRegistration.clear();
3217
3296
  this.bottomSheet.disconnect();
3297
+ this.unsubscribeLayout?.();
3298
+ this.unsubscribeLayout = null;
3218
3299
  super.disconnectedCallback();
3219
3300
  }
3220
3301
  updated(changed) {
@@ -3238,17 +3319,38 @@ var VioProductDetail = class extends LitElement {
3238
3319
  else this.revealActiveSlide();
3239
3320
  }
3240
3321
  this.measureLogo();
3322
+ this.announceLayout();
3241
3323
  if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
3242
3324
  this.slotRegistration.sync(this.slotAttributes());
3243
3325
  }
3244
3326
  }
3245
3327
  willUpdate(changed) {
3328
+ this.layoutNow = resolveDetailLayout(this.layout);
3246
3329
  if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
3247
3330
  if (changed.has("descriptionMode") || changed.has("open") && this.open) {
3248
3331
  const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
3249
3332
  this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
3250
3333
  }
3251
3334
  }
3335
+ /**
3336
+ * `vio:detail-layout-applied` (bubbles, composed), with the applied layout,
3337
+ * each time a product renders with a layout it has not been shown with yet:
3338
+ * every open of a product, and every change of layout while it is open.
3339
+ */
3340
+ announceLayout() {
3341
+ const product = this.product;
3342
+ if (!product || this.fetchError) return;
3343
+ const signature = layoutSignature(this.layoutNow);
3344
+ if (this.appliedFor && this.appliedFor.product === product && this.appliedFor.signature === signature) return;
3345
+ this.appliedFor = { product, signature };
3346
+ this.dispatchEvent(
3347
+ new CustomEvent("vio:detail-layout-applied", {
3348
+ bubbles: true,
3349
+ composed: true,
3350
+ detail: { ...copyAppliedLayout(this.layoutNow), productId: product.id, sponsorId: this.sponsorId }
3351
+ })
3352
+ );
3353
+ }
3252
3354
  /** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
3253
3355
  themeChoice(own, cssVar) {
3254
3356
  const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
@@ -3378,10 +3480,17 @@ var VioProductDetail = class extends LitElement {
3378
3480
  const price = Number(
3379
3481
  this.product.price?.amount_incl_taxes ?? this.product.price?.amount
3380
3482
  );
3483
+ const shown = layoutOriginOf(resolveDetailLayout(this.layout));
3381
3484
  Vio.analytics.track("view_item", {
3382
3485
  context: {
3383
3486
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
3384
- ...this.attribution()
3487
+ ...this.attribution(),
3488
+ ...shown.layoutId ? {
3489
+ layoutId: shown.layoutId,
3490
+ layoutRevision: shown.layoutRevision ?? "",
3491
+ experimentId: shown.experimentId,
3492
+ variantId: shown.variantId
3493
+ } : {}
3385
3494
  },
3386
3495
  commerce: {
3387
3496
  items: [
@@ -3712,7 +3821,8 @@ var VioProductDetail = class extends LitElement {
3712
3821
  this.selectionPrompt = missing.name;
3713
3822
  void this.updateComplete.then(() => {
3714
3823
  const group = this.renderRoot.querySelector(".option-group.needs-choice");
3715
- group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
3824
+ const apart = !!group?.closest(".options-apart");
3825
+ group?.scrollIntoView?.({ behavior: "smooth", block: apart ? "center" : "nearest" });
3716
3826
  });
3717
3827
  return false;
3718
3828
  }
@@ -3782,13 +3892,14 @@ var VioProductDetail = class extends LitElement {
3782
3892
  const variant = this.selectedVariant();
3783
3893
  const sponsorName = resolveSponsorIdentity(this.sponsorId)?.name || void 0;
3784
3894
  const { campaignId, slot, contentId, componentTemplateId, contentUrl, contentTitle } = this.attribution();
3895
+ const layout = layoutOriginOf(this.layoutNow);
3785
3896
  Vio.cart.addProduct({
3786
3897
  product: this.product,
3787
3898
  sponsorId: this.sponsorId,
3788
3899
  sponsorName,
3789
3900
  quantity: this.quantity,
3790
3901
  variantId: variant?.id,
3791
- origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle }
3902
+ origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle, ...layout }
3792
3903
  });
3793
3904
  return variant?.id;
3794
3905
  }
@@ -3807,7 +3918,9 @@ var VioProductDetail = class extends LitElement {
3807
3918
  sponsorId: this.sponsorId,
3808
3919
  quantity: this.quantity,
3809
3920
  variantId,
3810
- ...this.attribution()
3921
+ ...this.attribution(),
3922
+ // The detail layout of the add (0.34) — `variantId` above is the product's.
3923
+ ...this.layoutNow.source !== "default" ? { layout: layoutOriginOf(this.layoutNow) } : {}
3811
3924
  }
3812
3925
  })
3813
3926
  );
@@ -3920,7 +4033,7 @@ var VioProductDetail = class extends LitElement {
3920
4033
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3921
4034
  if (!this.requireSelection()) return;
3922
4035
  if (!(this.unitPrice > 0)) return;
3923
- if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
4036
+ if (this.cartHasLines() && this.vippsCartMode() === "checkout") {
3924
4037
  this.addCurrentToCart();
3925
4038
  this.close();
3926
4039
  this.openCheckoutWithVipps();
@@ -3932,12 +4045,46 @@ var VioProductDetail = class extends LitElement {
3932
4045
  void this.startVippsExpressOrCheckout(redirect);
3933
4046
  }
3934
4047
  /**
3935
- * The Vipps button will open the app with Express (see buyWithVipps): the
3936
- * brand has Express on, and this brand's cart is empty — with lines in it
3937
- * the checkout opens first.
4048
+ * The Vipps button will open the app with Express for this product alone
4049
+ * (see buyWithVipps): the brand has Express on, and this brand's cart is
4050
+ * empty.
3938
4051
  */
3939
4052
  vippsGoesStraightToApp() {
3940
- return this.vippsExpress && (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) === 0;
4053
+ return this.vippsExpress && !this.cartHasLines();
4054
+ }
4055
+ /** This brand's cart already has lines. */
4056
+ cartHasLines() {
4057
+ return (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
4058
+ }
4059
+ /** `vipps-cart-payment`, else the theme's, else `express`. */
4060
+ vippsCartMode() {
4061
+ return resolveVippsCartPayment(this, this.vippsCartPayment);
4062
+ }
4063
+ /**
4064
+ * What the Vipps button will pay when the brand's cart already has lines
4065
+ * (`express` mode): every unit in the cart plus this product at the chosen
4066
+ * quantity, and their total, as the cart sums it (shipping is chosen in
4067
+ * Vipps). null with an empty cart, or in `checkout` mode.
4068
+ */
4069
+ wholeCartToPay() {
4070
+ if (this.vippsCartMode() !== "express") return null;
4071
+ const cart = Vio.cart.getCart(this.sponsorId);
4072
+ const items = cart?.items ?? [];
4073
+ if (items.length === 0) return null;
4074
+ const units = items.reduce((n, i) => n + (Number(i.quantity) || 0), 0) + this.quantity;
4075
+ const total = items.reduce((sum, i) => sum + (Number(i.unitPrice) || 0) * (Number(i.quantity) || 0), 0) + this.unitPrice * this.quantity;
4076
+ return { units, total, currency: cart?.currency || this.currency };
4077
+ }
4078
+ /** The line under the Vipps button: what is paid (a cart with lines) or what happens in the app (Express, empty cart). */
4079
+ renderVippsNote() {
4080
+ const whole = this.wholeCartToPay();
4081
+ if (whole) {
4082
+ return html`<p class="vipps-note vipps-note-cart" part="vipps-note">Betaler hele handlekurven · ${whole.units} varer · ${formatPrice(whole.total, whole.currency)}</p>`;
4083
+ }
4084
+ if (this.vippsGoesStraightToApp()) {
4085
+ return html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>`;
4086
+ }
4087
+ return "";
3941
4088
  }
3942
4089
  async startVippsExpressOrCheckout(redirect) {
3943
4090
  let fallback = true;
@@ -4012,8 +4159,8 @@ var VioProductDetail = class extends LitElement {
4012
4159
  * after an await, so it cannot wait for the server's cart), while the
4013
4160
  * charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
4014
4161
  * what will be charged: an empty cart, and a sheet prepared for this
4015
- * product × quantity. Otherwise — lines already in the cart (as with Vipps,
4016
- * see buyWithVipps), or a quantity whose sheet is still being prepared —
4162
+ * product × quantity. Otherwise — lines already in the cart, or a quantity
4163
+ * whose sheet is still being prepared —
4017
4164
  * the product is added and the checkout opens with Apple Pay preselected:
4018
4165
  * every line and the total on screen, and its own Apple Pay button pays
4019
4166
  * that total.
@@ -4113,11 +4260,16 @@ var VioProductDetail = class extends LitElement {
4113
4260
  * the heading, the text or «Les mer» opens the rest (Angelo, 2026-10-28).
4114
4261
  * A description that fits in the preview has nothing to open.
4115
4262
  */
4263
+ /** The description fold's heading: this element's, else the page's, else «Beskrivelse». */
4264
+ descriptionHeading() {
4265
+ const own = this.descriptionLabel.trim();
4266
+ return (own && own.length <= 40 ? own : "") || currentGlobalDescriptionLabel() || "Beskrivelse";
4267
+ }
4116
4268
  renderDescription(text) {
4117
4269
  const mode = this.resolvedDescriptionMode;
4118
4270
  if (mode === "open") {
4119
4271
  return html`<div class="description-block description-preview open" part="description">
4120
- <div class="desc-head">Beskrivelse</div>
4272
+ <div class="desc-head">${this.descriptionHeading()}</div>
4121
4273
  <div class="description">${text}</div>
4122
4274
  </div>`;
4123
4275
  }
@@ -4127,7 +4279,7 @@ var VioProductDetail = class extends LitElement {
4127
4279
  if (foldable) this.descriptionOpen = !this.descriptionOpen;
4128
4280
  };
4129
4281
  return html`<div class="description-block description-preview ${open ? "open" : ""} ${foldable ? "clipped" : ""} ${mode}" part="description">
4130
- <button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>Beskrivelse</button>
4282
+ <button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>${this.descriptionHeading()}</button>
4131
4283
  ${mode === "folded" && !open ? nothing : html`<div class="description ${open ? "" : "collapsed"}" @click=${() => !this.descriptionOpen && toggle()}>${text}</div>`}
4132
4284
  ${mode === "preview" && this.descriptionClipped ? html`<button class="desc-more" type="button" @click=${toggle}>${this.descriptionOpen ? "Vis mindre" : "Les mer"}</button>` : nothing}
4133
4285
  </div>`;
@@ -4144,19 +4296,7 @@ var VioProductDetail = class extends LitElement {
4144
4296
  }
4145
4297
  renderHighlights() {
4146
4298
  const own = (Array.isArray(this.highlights) ? this.highlights : []).filter((h) => h && h.title);
4147
- const items = own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? [];
4148
- if (items.length === 0) return nothing;
4149
- return html`<ul class="highlights" part="highlights">
4150
- ${items.map((h) => {
4151
- const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
4152
- const icon = path ? html`<svg class="hl-icon" viewBox="0 0 24 24" aria-hidden="true"><path d=${path}></path></svg>` : h.icon ? html`<img class="hl-icon" src=${h.icon} alt="" />` : nothing;
4153
- return html`<li class="hl" part="highlight">
4154
- ${icon}
4155
- <span class="hl-title">${h.title}</span>
4156
- ${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
4157
- </li>`;
4158
- })}
4159
- </ul>`;
4299
+ return renderHighlights(own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? []);
4160
4300
  }
4161
4301
  /**
4162
4302
  * «Frakt & retur», folded like «Beskrivelse»: the delivery options for the
@@ -4318,9 +4458,6 @@ var VioProductDetail = class extends LitElement {
4318
4458
  }
4319
4459
  if (!this.product) return html``;
4320
4460
  const p = this.product;
4321
- const stock = this.stockLabel();
4322
- const compare = this.compareAt;
4323
- const showCompare = compare !== null && compare > this.unitPrice;
4324
4461
  const images = this.allImages();
4325
4462
  const activeImage = this.activeImage;
4326
4463
  return html`
@@ -4365,24 +4502,110 @@ var VioProductDetail = class extends LitElement {
4365
4502
  </div>`}
4366
4503
 
4367
4504
  <div class="info" part="info">
4368
- ${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
4369
- <div class="headline">
4370
- <div class="title-row">
4371
- <h2 class="name" part="title">${p.title}</h2>
4372
- <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
4373
- </div>
4374
- <div class="price-row">
4505
+ ${this.renderSections(this.layoutNow.sections)}
4506
+ </div>
4507
+ </div>
4508
+ `;
4509
+ }
4510
+ /**
4511
+ * The info column in the applied layout's order (0.34) — the real DOM in
4512
+ * that order, never CSS `order`, the same on every screen. The default
4513
+ * order renders exactly the column before 0.34: brand, `.headline` (the
4514
+ * title row with the stock, the price row), then `.purchase` — `.buy` with
4515
+ * the options first — and highlights, description, «Frakt & retur» in it.
4516
+ *
4517
+ * A run of `title` / `stock` / `price` is one `.headline`; `title`
4518
+ * right before `stock` share the title row. `options` right before
4519
+ * `purchase` live in `.buy` (a pinned buy zone keeps them); elsewhere they
4520
+ * are a block of their own. Everything after `purchase` goes in
4521
+ * `.purchase`, as the description does by default: a pinned buy zone may
4522
+ * slide over it, never over what comes before.
4523
+ */
4524
+ renderSections(sections) {
4525
+ const out = [];
4526
+ for (let i = 0; i < sections.length; i++) {
4527
+ const s = sections[i];
4528
+ if (s === "title" || s === "stock" || s === "price") {
4529
+ let j = i;
4530
+ while (j < sections.length && ["title", "stock", "price"].includes(sections[j])) j++;
4531
+ out.push(this.renderHeadline(sections.slice(i, j)));
4532
+ i = j - 1;
4533
+ continue;
4534
+ }
4535
+ if (s === "options" && sections[i + 1] === "purchase") continue;
4536
+ if (s === "purchase") {
4537
+ const optionsInBuy = sections[i - 1] === "options";
4538
+ out.push(this.renderPurchase(optionsInBuy, sections.slice(i + 1)));
4539
+ break;
4540
+ }
4541
+ out.push(this.renderSection(s));
4542
+ }
4543
+ return out;
4544
+ }
4545
+ /** A run of title / stock / price: one `.headline`. */
4546
+ renderHeadline(run) {
4547
+ const p = this.product;
4548
+ const stock = this.stockLabel();
4549
+ const compare = this.compareAt;
4550
+ const showCompare = compare !== null && compare > this.unitPrice;
4551
+ const name = html`<h2 class="name" part="title">${p.title}</h2>`;
4552
+ const stockLine = html`<div class="stock ${stock.cls}" part="stock">${stock.text}</div>`;
4553
+ const parts = [];
4554
+ for (let i = 0; i < run.length; i++) {
4555
+ const s = run[i];
4556
+ if (s === "title" && run[i + 1] === "stock") {
4557
+ parts.push(html`<div class="title-row">
4558
+ ${name}
4559
+ ${stockLine}
4560
+ </div>`);
4561
+ i++;
4562
+ } else if (s === "title") {
4563
+ parts.push(html`<div class="title-row">${name}</div>`);
4564
+ } else if (s === "stock") {
4565
+ parts.push(stockLine);
4566
+ } else {
4567
+ parts.push(html`<div class="price-row">
4375
4568
  <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
4376
4569
  ${showCompare ? html`<span class="compare-at">${formatPrice(
4377
- compare,
4378
- p.price.currency_code
4379
- )}</span>` : ""}
4380
- </div>
4381
- </div>
4382
-
4383
- <div class="purchase">
4570
+ compare,
4571
+ p.price.currency_code
4572
+ )}</span>` : ""}
4573
+ </div>`);
4574
+ }
4575
+ }
4576
+ return html`<div class="headline">
4577
+ ${parts}
4578
+ </div>`;
4579
+ }
4580
+ /** One section that is not the headline nor the purchase group. */
4581
+ renderSection(section) {
4582
+ const p = this.product;
4583
+ switch (section) {
4584
+ case "brand":
4585
+ return p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : "";
4586
+ case "options":
4587
+ return p.options.length > 0 ? html`<div class="buy-options options-apart" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : "";
4588
+ case "highlights":
4589
+ return this.renderHighlights();
4590
+ case "description":
4591
+ return p.description ? this.renderDescription(this.stripHtml(p.description)) : "";
4592
+ case "shippingReturns":
4593
+ return this.renderShippingReturns();
4594
+ default:
4595
+ return "";
4596
+ }
4597
+ }
4598
+ /**
4599
+ * The SDK-managed buy group, never split: «Frakt fra…», Vipps / Apple Pay /
4600
+ * the other ways to pay, quantity + «Legg i handlekurv», «Du handler direkte
4601
+ * hos …». The options when they come right before it; then what follows it.
4602
+ */
4603
+ renderPurchase(optionsInBuy, rest) {
4604
+ const p = this.product;
4605
+ const optionsApart = !optionsInBuy && p.options.length > 0;
4606
+ return html`<div class=${optionsApart ? "purchase options-apart" : "purchase"}>
4384
4607
  <div class="buy" part="buy">
4385
- ${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
4608
+ ${optionsInBuy && p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
4386
4609
  ${this.renderDeliveryNote()}
4387
4610
  ${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
4388
4611
  ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
@@ -4411,7 +4634,7 @@ var VioProductDetail = class extends LitElement {
4411
4634
  <span class="vipps-badge">vipps</span>
4412
4635
  </button>
4413
4636
  ` : ""}
4414
- ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") && this.vippsGoesStraightToApp() ? html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>` : ""}
4637
+ ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.renderVippsNote() : ""}
4415
4638
 
4416
4639
  <div class="qty-cta-row" part="quantity-row">
4417
4640
  <div class="qty-stepper">
@@ -4499,14 +4722,8 @@ var VioProductDetail = class extends LitElement {
4499
4722
  ${this.renderSellerNote()}
4500
4723
  </div>
4501
4724
 
4502
- ${this.renderHighlights()}
4503
-
4504
- ${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
4505
- ${this.renderShippingReturns()}
4506
- </div>
4507
- </div>
4508
- </div>
4509
- `;
4725
+ ${this.renderSections(rest)}
4726
+ </div>`;
4510
4727
  }
4511
4728
  renderOptionPickers(options) {
4512
4729
  if (options.length === 0) return "";
@@ -4547,8 +4764,8 @@ var VioProductDetail = class extends LitElement {
4547
4764
  })}`;
4548
4765
  }
4549
4766
  /** Best-effort HTML strip for description. */
4550
- stripHtml(html7) {
4551
- return html7.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
4767
+ stripHtml(html8) {
4768
+ return html8.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
4552
4769
  }
4553
4770
  };
4554
4771
  __decorateClass([
@@ -4590,12 +4807,18 @@ __decorateClass([
4590
4807
  __decorateClass([
4591
4808
  property({ type: Boolean, attribute: "no-added-toast" })
4592
4809
  ], VioProductDetail.prototype, "noAddedToast", 2);
4810
+ __decorateClass([
4811
+ property({ type: String, attribute: "vipps-cart-payment" })
4812
+ ], VioProductDetail.prototype, "vippsCartPayment", 2);
4593
4813
  __decorateClass([
4594
4814
  property({ type: String })
4595
4815
  ], VioProductDetail.prototype, "gallery", 2);
4596
4816
  __decorateClass([
4597
4817
  property({ type: String, attribute: "description-mode" })
4598
4818
  ], VioProductDetail.prototype, "descriptionMode", 2);
4819
+ __decorateClass([
4820
+ property({ type: String, attribute: "description-label" })
4821
+ ], VioProductDetail.prototype, "descriptionLabel", 2);
4599
4822
  __decorateClass([
4600
4823
  property({ type: Array })
4601
4824
  ], VioProductDetail.prototype, "highlights", 2);
@@ -4605,6 +4828,9 @@ __decorateClass([
4605
4828
  __decorateClass([
4606
4829
  property({ type: Object, attribute: "carrier-logos" })
4607
4830
  ], VioProductDetail.prototype, "carrierLogos", 2);
4831
+ __decorateClass([
4832
+ property({ attribute: "layout", converter: layoutAttribute })
4833
+ ], VioProductDetail.prototype, "layout", 2);
4608
4834
  __decorateClass([
4609
4835
  state()
4610
4836
  ], VioProductDetail.prototype, "product", 2);
@@ -4724,6 +4950,12 @@ function cartShipping(items, currency) {
4724
4950
  }
4725
4951
  return null;
4726
4952
  }
4953
+ function cartShippingIsFrom(items, currency) {
4954
+ if (items.some((item) => ratePrice(item.shipping, currency) !== null)) return false;
4955
+ return items.some(
4956
+ (item) => (item.availableShippings ?? []).filter((r) => ratePrice(r, currency) !== null).length > 1
4957
+ );
4958
+ }
4727
4959
  var VioCart = class extends LitElement {
4728
4960
  constructor() {
4729
4961
  super(...arguments);
@@ -4731,12 +4963,15 @@ var VioCart = class extends LitElement {
4731
4963
  this.heading = "Handlekurv";
4732
4964
  this.emptyLabel = "Handlekurven er tom";
4733
4965
  this.subtotalLabel = "Sum";
4966
+ this.vippsCartPayment = "";
4734
4967
  this.carts = /* @__PURE__ */ new Map();
4735
4968
  this.itemCount = 0;
4736
4969
  this.applePayAvailable = false;
4737
4970
  this.availableMethods = null;
4738
4971
  this.methodsLoading = false;
4739
4972
  this.vippsOfficial = false;
4973
+ /** A Vipps payment is being started from here: one at a time. */
4974
+ this.vippsStarting = false;
4740
4975
  /** Whether the method lookup has been attempted — see boundOnCartChange. */
4741
4976
  this.paymentMethodsAttempted = false;
4742
4977
  this.confirmedOrder = null;
@@ -4772,7 +5007,8 @@ var VioCart = class extends LitElement {
4772
5007
  };
4773
5008
  }
4774
5009
  static {
4775
- this.styles = css`
5010
+ this.styles = [
5011
+ css`
4776
5012
  :host { display: contents; }
4777
5013
 
4778
5014
  .backdrop {
@@ -4991,6 +5227,12 @@ var VioCart = class extends LitElement {
4991
5227
  font-size: 14px;
4992
5228
  }
4993
5229
  .total-row { font-weight: 700; font-size: 16px; margin-top: 0; }
5230
+ /* Where the shipping method is chosen: in Vipps, or in the checkout. */
5231
+ .shipping-hint {
5232
+ margin: calc(var(--vio-space-md, 16px) * -0.5) 0 var(--vio-space-md, 16px);
5233
+ font-size: 12px;
5234
+ color: var(--vio-color-text-secondary, #666);
5235
+ }
4994
5236
  .subtotal-value { font-weight: 700; font-size: 16px; }
4995
5237
 
4996
5238
  /* Apple Pay express — pay the whole cart directly. */
@@ -5148,7 +5390,9 @@ var VioCart = class extends LitElement {
5148
5390
  font-family: var(--vio-font-sans, -apple-system, sans-serif);
5149
5391
  }
5150
5392
  .confirm-close:hover { background: #222; }
5151
- `;
5393
+ `,
5394
+ highlightsStyles("cart")
5395
+ ];
5152
5396
  }
5153
5397
  connectedCallback() {
5154
5398
  super.connectedCallback();
@@ -5312,7 +5556,6 @@ var VioCart = class extends LitElement {
5312
5556
  })
5313
5557
  );
5314
5558
  }
5315
- /** Vipps — opens the checkout with Vipps preselected. */
5316
5559
  /**
5317
5560
  * Open the checkout without preselecting a method.
5318
5561
  *
@@ -5334,21 +5577,73 @@ var VioCart = class extends LitElement {
5334
5577
  );
5335
5578
  }
5336
5579
  /**
5337
- * Vipps from the cart: the checkout opens with Vipps preselected — the
5338
- * standard path, where our form collects name, address, mobile and the
5339
- * delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
5340
- * It no longer starts Express from here: Express is the product detail's
5341
- * «Kjøp nå med Vipps» only.
5580
+ * Vipps from the cart (2026-10-29: «el botón negro va con el nuestro y el
5581
+ * botón de Vipps paga el carrito entero»): Vipps Express for the sponsor's
5582
+ * WHOLE cart — the same Express as the detail's «Kjøp nå med Vipps»: the
5583
+ * hand-over overlay, `vio:payment-redirect-*` with source `'cart'`, the
5584
+ * return and its confirmation. «Til kassen» is the way into our checkout.
5585
+ * When the brand has Express off, or this cart cannot have it (several
5586
+ * suppliers, no rate), the checkout opens with Vipps preselected.
5587
+ *
5588
+ * `vipps-cart-payment="checkout"` keeps the 0.31–0.34 behaviour: the
5589
+ * checkout opens with Vipps preselected, the standard payment.
5342
5590
  */
5343
5591
  onVipps() {
5344
5592
  if (!this.payable) return;
5345
- const firstSponsorId = [...this.carts.keys()][0];
5346
- if (firstSponsorId === void 0) return;
5593
+ if (resolveVippsCartPayment(this, this.vippsCartPayment) === "checkout") {
5594
+ const firstSponsorId = [...this.carts.keys()][0];
5595
+ if (firstSponsorId === void 0) return;
5596
+ this.openCheckoutWithVipps(firstSponsorId);
5597
+ return;
5598
+ }
5599
+ if (this.vippsStarting) return;
5600
+ const sponsorId = this.vippsSponsorId();
5601
+ if (sponsorId === void 0) return;
5602
+ this.vippsStarting = true;
5603
+ const redirect = beginPaymentRedirect({ method: "vipps", sponsorId, source: "cart" });
5604
+ void this.startVippsExpressOrCheckout(sponsorId, redirect).finally(() => {
5605
+ this.vippsStarting = false;
5606
+ });
5607
+ }
5608
+ /**
5609
+ * The one sponsor whose cart the Vipps button pays: the first with
5610
+ * something to pay for. Express is built on THAT sponsor's backend cart
5611
+ * only — another sponsor's lines are never in it.
5612
+ */
5613
+ vippsSponsorId() {
5614
+ for (const cart of this.carts.values()) {
5615
+ const total = cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
5616
+ if (cart.items.length > 0 && total > 0) return cart.sponsorId;
5617
+ }
5618
+ return void 0;
5619
+ }
5620
+ async startVippsExpressOrCheckout(sponsorId, redirect) {
5621
+ let fallback = true;
5622
+ try {
5623
+ await Vio.cart.waitForCartMutations(sponsorId);
5624
+ if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
5625
+ redirect.abort("express-off");
5626
+ } else {
5627
+ const res = await Vio.checkout.startVippsExpress(sponsorId);
5628
+ fallback = !res?.payment_url;
5629
+ if (fallback) redirect.abort("not-started");
5630
+ }
5631
+ } catch (err) {
5632
+ if (typeof console !== "undefined") {
5633
+ console.warn("[VioCart] Vipps Express not started:", err);
5634
+ }
5635
+ redirect.fail(err instanceof Error ? err.message : String(err));
5636
+ }
5637
+ if (!fallback) return;
5638
+ this.openCheckoutWithVipps(sponsorId);
5639
+ }
5640
+ /** Our checkout with Vipps preselected: the host opens it on `vio:checkout-open`. */
5641
+ openCheckoutWithVipps(sponsorId) {
5347
5642
  this.dispatchEvent(
5348
5643
  new CustomEvent("vio:checkout-open", {
5349
5644
  bubbles: true,
5350
5645
  composed: true,
5351
- detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
5646
+ detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
5352
5647
  })
5353
5648
  );
5354
5649
  }
@@ -5558,23 +5853,39 @@ var VioCart = class extends LitElement {
5558
5853
  <span class="vipps-logo">vipps</span>
5559
5854
  </button>
5560
5855
  ` : ""}
5856
+ ${this.renderBrandHighlights()}
5561
5857
  </div>
5562
5858
  `}
5563
5859
  </aside>
5564
5860
  `;
5565
5861
  }
5862
+ /**
5863
+ * The selling brand's promises under the ways to pay, as in the product
5864
+ * detail (one article = one brand: the cart's brand). Hidden with the theme's
5865
+ * `cartHighlightsDisplay: 'none'`; nothing when the brand has none.
5866
+ */
5867
+ renderBrandHighlights() {
5868
+ const sponsorId = [...this.carts.keys()].find((id) => (this.carts.get(id)?.items.length ?? 0) > 0);
5869
+ if (sponsorId === void 0) return "";
5870
+ return renderHighlights(resolveSponsorIdentity(sponsorId)?.highlights ?? []);
5871
+ }
5566
5872
  /** «Frakt» (the chosen or cheapest rate) and «Totalt» with it; «Beregnes i kassen» when unknown. */
5567
5873
  renderShippingRows() {
5568
5874
  const shipping = this.shippingAcrossSponsors;
5875
+ const from = shipping !== null && shipping > 0 && [...this.carts.values()].some(
5876
+ (cart) => cart.items.length > 0 && cartShippingIsFrom(cart.items, cart.currency)
5877
+ );
5878
+ const fra = from ? "fra " : "";
5569
5879
  return html`
5570
5880
  <div class="shipping-row" part="cart-shipping">
5571
5881
  <span class="subtotal-label">Frakt</span>
5572
- <span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : formatPrice(shipping, this.primaryCurrency)}</span>
5882
+ <span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : `${fra}${formatPrice(shipping, this.primaryCurrency)}`}</span>
5573
5883
  </div>
5574
5884
  ${shipping !== null ? html`<div class="total-row" part="cart-total">
5575
5885
  <span>Totalt</span>
5576
- <span>${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
5886
+ <span>${fra}${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
5577
5887
  </div>` : ""}
5888
+ <p class="shipping-hint" part="cart-shipping-hint">Frakt velger du i Vipps eller i neste steg.</p>
5578
5889
  `;
5579
5890
  }
5580
5891
  renderConfirmation() {
@@ -5616,6 +5927,9 @@ __decorateClass([
5616
5927
  __decorateClass([
5617
5928
  property({ type: String, attribute: "subtotal-label" })
5618
5929
  ], VioCart.prototype, "subtotalLabel", 2);
5930
+ __decorateClass([
5931
+ property({ type: String, attribute: "vipps-cart-payment" })
5932
+ ], VioCart.prototype, "vippsCartPayment", 2);
5619
5933
  __decorateClass([
5620
5934
  state()
5621
5935
  ], VioCart.prototype, "carts", 2);
@@ -9391,5 +9705,5 @@ function registerVioElements() {
9391
9705
  }
9392
9706
 
9393
9707
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
9394
- //# sourceMappingURL=chunk-T5B74YBQ.js.map
9395
- //# sourceMappingURL=chunk-T5B74YBQ.js.map
9708
+ //# sourceMappingURL=chunk-DEREQRCM.js.map
9709
+ //# sourceMappingURL=chunk-DEREQRCM.js.map