@vio-live/web-sdk 0.27.0 → 0.29.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/CHANGELOG.md +359 -0
- package/README.md +185 -0
- package/dist/{chunk-HJRIPCWB.js → chunk-AG4G2S2M.js} +442 -37
- package/dist/chunk-AG4G2S2M.js.map +1 -0
- package/dist/{chunk-GLAKUYQA.js → chunk-L6YYYKQK.js} +905 -188
- package/dist/chunk-L6YYYKQK.js.map +1 -0
- package/dist/core/index.d.ts +187 -20
- package/dist/core/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +3 -3
- package/dist/{tokens-CihuCU2l.d.ts → tokens-Dok4d21V.d.ts} +131 -9
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/package.json +3 -2
- package/dist/chunk-GLAKUYQA.js.map +0 -1
- package/dist/chunk-HJRIPCWB.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-AG4G2S2M.js';
|
|
2
2
|
import { LitElement, css, html, nothing } from 'lit';
|
|
3
3
|
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
@@ -151,7 +151,38 @@ var THEME_KEY_TO_CSS_VAR = {
|
|
|
151
151
|
spaceSm: "--vio-space-sm",
|
|
152
152
|
spaceMd: "--vio-space-md",
|
|
153
153
|
spaceLg: "--vio-space-lg",
|
|
154
|
-
spaceXl: "--vio-space-xl"
|
|
154
|
+
spaceXl: "--vio-space-xl",
|
|
155
|
+
detailWidth: "--vio-detail-width",
|
|
156
|
+
detailPaddingX: "--vio-detail-padding-x",
|
|
157
|
+
detailLogoHeight: "--vio-detail-logo-height",
|
|
158
|
+
detailLogoZoom: "--vio-detail-logo-zoom",
|
|
159
|
+
detailTitleSize: "--vio-detail-title-size",
|
|
160
|
+
detailPriceSize: "--vio-detail-price-size",
|
|
161
|
+
detailImageFit: "--vio-detail-image-fit",
|
|
162
|
+
detailImageAspect: "--vio-detail-image-aspect",
|
|
163
|
+
detailImageMaxHeight: "--vio-detail-image-max-height",
|
|
164
|
+
detailImageZoom: "--vio-detail-image-zoom",
|
|
165
|
+
detailImageInset: "--vio-detail-image-inset",
|
|
166
|
+
detailImageBg: "--vio-detail-image-bg",
|
|
167
|
+
detailThumbSize: "--vio-detail-thumb-size",
|
|
168
|
+
detailThumbGap: "--vio-detail-thumb-gap",
|
|
169
|
+
detailThumbRadius: "--vio-detail-thumb-radius",
|
|
170
|
+
detailThumbActiveColor: "--vio-detail-thumb-active-color",
|
|
171
|
+
detailOptionHeight: "--vio-detail-option-height",
|
|
172
|
+
detailOptionGap: "--vio-detail-option-gap",
|
|
173
|
+
detailOptionRadius: "--vio-detail-option-radius",
|
|
174
|
+
detailOptionWrap: "--vio-detail-option-wrap",
|
|
175
|
+
detailButtonHeight: "--vio-detail-button-height",
|
|
176
|
+
detailButtonRadius: "--vio-detail-button-radius",
|
|
177
|
+
detailBuyPosition: "--vio-detail-buy-position",
|
|
178
|
+
detailBuyShadow: "--vio-detail-buy-shadow",
|
|
179
|
+
cartWidth: "--vio-cart-width",
|
|
180
|
+
checkoutWidth: "--vio-checkout-width",
|
|
181
|
+
confirmLogoHeight: "--vio-confirm-logo-height",
|
|
182
|
+
confirmTitleSize: "--vio-confirm-title-size",
|
|
183
|
+
confirmCheckBg: "--vio-confirm-check-bg",
|
|
184
|
+
redirectBg: "--vio-redirect-bg",
|
|
185
|
+
redirectSpinner: "--vio-redirect-spinner"
|
|
155
186
|
};
|
|
156
187
|
function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
|
|
157
188
|
if (!target) return;
|
|
@@ -1244,6 +1275,102 @@ __decorateClass([
|
|
|
1244
1275
|
state()
|
|
1245
1276
|
], VioProductCarousel.prototype, "fetchError", 2);
|
|
1246
1277
|
|
|
1278
|
+
// src/ui/payment-redirect.ts
|
|
1279
|
+
var OVERLAY_CLASS = "vio-redirect-overlay";
|
|
1280
|
+
var STYLE_ID = "vio-redirect-overlay-style";
|
|
1281
|
+
var LABEL = { vipps: "Vipps", stripe: "Stripe", klarna: "Klarna" };
|
|
1282
|
+
function emit(name, detail, cancelable = false) {
|
|
1283
|
+
if (typeof document === "undefined") return true;
|
|
1284
|
+
return document.dispatchEvent(new CustomEvent(name, { detail, cancelable }));
|
|
1285
|
+
}
|
|
1286
|
+
function injectStyle() {
|
|
1287
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
1288
|
+
const style = document.createElement("style");
|
|
1289
|
+
style.id = STYLE_ID;
|
|
1290
|
+
style.textContent = `
|
|
1291
|
+
.${OVERLAY_CLASS} {
|
|
1292
|
+
position: fixed; inset: 0; z-index: 2147483000;
|
|
1293
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
|
|
1294
|
+
background: var(--vio-redirect-bg, rgba(255, 255, 255, 0.94));
|
|
1295
|
+
color: var(--vio-redirect-color, var(--vio-color-text, #0a0a0a));
|
|
1296
|
+
font: 600 16px/1.3 var(--vio-redirect-font, var(--vio-font-sans, -apple-system, sans-serif));
|
|
1297
|
+
opacity: 0; transition: opacity 0.18s ease; cursor: progress;
|
|
1298
|
+
}
|
|
1299
|
+
.${OVERLAY_CLASS}.is-shown { opacity: 1; }
|
|
1300
|
+
.${OVERLAY_CLASS} .vio-redirect-spinner {
|
|
1301
|
+
width: 36px; height: 36px; border-radius: 50%;
|
|
1302
|
+
border: 3px solid rgba(0, 0, 0, 0.1); border-top-color: var(--vio-redirect-spinner, #ff5b24);
|
|
1303
|
+
animation: vio-redirect-spin 0.8s linear infinite;
|
|
1304
|
+
}
|
|
1305
|
+
@keyframes vio-redirect-spin { to { transform: rotate(360deg); } }
|
|
1306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1307
|
+
.${OVERLAY_CLASS} { transition: none; }
|
|
1308
|
+
.${OVERLAY_CLASS} .vio-redirect-spinner { animation: none; }
|
|
1309
|
+
}`;
|
|
1310
|
+
document.head.appendChild(style);
|
|
1311
|
+
}
|
|
1312
|
+
var current = null;
|
|
1313
|
+
function removeOverlay() {
|
|
1314
|
+
const el = current?.el;
|
|
1315
|
+
if (!el) return;
|
|
1316
|
+
el.classList.remove("is-shown");
|
|
1317
|
+
setTimeout(() => el.remove(), 200);
|
|
1318
|
+
current.el = null;
|
|
1319
|
+
}
|
|
1320
|
+
function onPageShow(e) {
|
|
1321
|
+
if (!e.persisted || !current || current.settled) return;
|
|
1322
|
+
current.settled = true;
|
|
1323
|
+
removeOverlay();
|
|
1324
|
+
emit("vio:payment-redirect-cancel", { ...current.detail, reason: "back" });
|
|
1325
|
+
current = null;
|
|
1326
|
+
}
|
|
1327
|
+
function beginPaymentRedirect(detail) {
|
|
1328
|
+
if (current && !current.settled) {
|
|
1329
|
+
current.settled = true;
|
|
1330
|
+
removeOverlay();
|
|
1331
|
+
}
|
|
1332
|
+
const hostTookOver = !emit("vio:payment-redirect-start", detail, true);
|
|
1333
|
+
const state6 = { el: null, detail, settled: false };
|
|
1334
|
+
current = state6;
|
|
1335
|
+
if (!hostTookOver && typeof document !== "undefined" && document.body) {
|
|
1336
|
+
injectStyle();
|
|
1337
|
+
const el = document.createElement("div");
|
|
1338
|
+
el.className = OVERLAY_CLASS;
|
|
1339
|
+
el.setAttribute("role", "status");
|
|
1340
|
+
el.setAttribute("aria-live", "polite");
|
|
1341
|
+
const spinner = document.createElement("div");
|
|
1342
|
+
spinner.className = "vio-redirect-spinner";
|
|
1343
|
+
spinner.setAttribute("aria-hidden", "true");
|
|
1344
|
+
const text = document.createElement("div");
|
|
1345
|
+
text.className = "vio-redirect-text";
|
|
1346
|
+
text.textContent = `\xC5pner ${LABEL[detail.method] ?? detail.method} \u2026`;
|
|
1347
|
+
el.append(spinner, text);
|
|
1348
|
+
el.addEventListener("click", (e) => e.stopPropagation());
|
|
1349
|
+
document.body.appendChild(el);
|
|
1350
|
+
requestAnimationFrame(() => el.classList.add("is-shown"));
|
|
1351
|
+
state6.el = el;
|
|
1352
|
+
}
|
|
1353
|
+
if (typeof window !== "undefined") window.addEventListener("pageshow", onPageShow);
|
|
1354
|
+
const settle = (event, reason) => {
|
|
1355
|
+
if (state6.settled) return;
|
|
1356
|
+
state6.settled = true;
|
|
1357
|
+
if (current === state6) {
|
|
1358
|
+
removeOverlay();
|
|
1359
|
+
current = null;
|
|
1360
|
+
} else if (state6.el) {
|
|
1361
|
+
state6.el.remove();
|
|
1362
|
+
}
|
|
1363
|
+
emit(event, { ...detail, reason });
|
|
1364
|
+
};
|
|
1365
|
+
return {
|
|
1366
|
+
fail: (reason) => settle("vio:payment-redirect-error", reason),
|
|
1367
|
+
abort: (reason = "not-started") => settle("vio:payment-redirect-cancel", reason)
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function returnCancelled(detail) {
|
|
1371
|
+
emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1247
1374
|
// src/core/checkout/method-taxonomy.ts
|
|
1248
1375
|
function normalizeMethodName(name) {
|
|
1249
1376
|
return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -1451,6 +1578,207 @@ function isWithin(node, ancestor) {
|
|
|
1451
1578
|
return false;
|
|
1452
1579
|
}
|
|
1453
1580
|
|
|
1581
|
+
// src/ui/bottom-sheet.ts
|
|
1582
|
+
var PHONE = "(max-width: 600px)";
|
|
1583
|
+
var SLOP = 6;
|
|
1584
|
+
var CLOSE_DISTANCE = 120;
|
|
1585
|
+
var FLICK_VELOCITY = 0.5;
|
|
1586
|
+
var FLICK_MIN = 24;
|
|
1587
|
+
var CLICK_AFTER_DRAG_MS = 400;
|
|
1588
|
+
function isPhoneSheet() {
|
|
1589
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(PHONE).matches;
|
|
1590
|
+
}
|
|
1591
|
+
var BottomSheet = class {
|
|
1592
|
+
constructor(opts) {
|
|
1593
|
+
this.opts = opts;
|
|
1594
|
+
this.active = false;
|
|
1595
|
+
this.locked = false;
|
|
1596
|
+
this.drag = null;
|
|
1597
|
+
this.dragEndedAt = -Infinity;
|
|
1598
|
+
this.onPointerDown = (e) => {
|
|
1599
|
+
if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
|
|
1600
|
+
if (this.drag || !isPhoneSheet()) return;
|
|
1601
|
+
const sheet = this.opts.sheet();
|
|
1602
|
+
if (!sheet || !inZone(e, sheet, this.opts.dragZones)) return;
|
|
1603
|
+
this.drag = {
|
|
1604
|
+
pointerId: e.pointerId,
|
|
1605
|
+
startY: e.clientY,
|
|
1606
|
+
lastY: e.clientY,
|
|
1607
|
+
lastT: e.timeStamp,
|
|
1608
|
+
velocity: 0,
|
|
1609
|
+
moving: false,
|
|
1610
|
+
offset: 0
|
|
1611
|
+
};
|
|
1612
|
+
window.addEventListener("pointermove", this.onPointerMove);
|
|
1613
|
+
window.addEventListener("pointerup", this.onPointerUp);
|
|
1614
|
+
window.addEventListener("pointercancel", this.onPointerCancel);
|
|
1615
|
+
};
|
|
1616
|
+
this.onPointerMove = (e) => {
|
|
1617
|
+
const drag = this.drag;
|
|
1618
|
+
if (!drag || e.pointerId !== drag.pointerId) return;
|
|
1619
|
+
const dy = e.clientY - drag.startY;
|
|
1620
|
+
if (!drag.moving) {
|
|
1621
|
+
if (dy < -SLOP) return this.abandon();
|
|
1622
|
+
if (dy <= SLOP) return;
|
|
1623
|
+
drag.moving = true;
|
|
1624
|
+
const sheet2 = this.opts.sheet();
|
|
1625
|
+
if (sheet2) sheet2.style.transition = "none";
|
|
1626
|
+
const backdrop2 = this.opts.backdrop?.();
|
|
1627
|
+
if (backdrop2) backdrop2.style.transition = "none";
|
|
1628
|
+
}
|
|
1629
|
+
const dt = e.timeStamp - drag.lastT;
|
|
1630
|
+
if (dt > 0) drag.velocity = 0.8 * ((e.clientY - drag.lastY) / dt) + 0.2 * drag.velocity;
|
|
1631
|
+
drag.lastY = e.clientY;
|
|
1632
|
+
drag.lastT = e.timeStamp;
|
|
1633
|
+
drag.offset = Math.max(0, dy);
|
|
1634
|
+
const sheet = this.opts.sheet();
|
|
1635
|
+
if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
|
|
1636
|
+
const backdrop = this.opts.backdrop?.();
|
|
1637
|
+
if (backdrop && sheet) {
|
|
1638
|
+
const height = sheet.getBoundingClientRect().height || window.innerHeight;
|
|
1639
|
+
backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
|
|
1640
|
+
}
|
|
1641
|
+
if (e.cancelable) e.preventDefault();
|
|
1642
|
+
};
|
|
1643
|
+
this.onPointerUp = (e) => {
|
|
1644
|
+
const drag = this.drag;
|
|
1645
|
+
if (!drag || e.pointerId !== drag.pointerId) return;
|
|
1646
|
+
this.stopTracking();
|
|
1647
|
+
this.drag = null;
|
|
1648
|
+
if (!drag.moving) return;
|
|
1649
|
+
this.dragEndedAt = performance.now();
|
|
1650
|
+
const sheet = this.opts.sheet();
|
|
1651
|
+
const height = sheet?.getBoundingClientRect().height || window.innerHeight;
|
|
1652
|
+
const far = drag.offset >= Math.min(CLOSE_DISTANCE, height / 4);
|
|
1653
|
+
const flick = drag.velocity >= FLICK_VELOCITY && drag.offset >= FLICK_MIN;
|
|
1654
|
+
const allowed = !this.opts.canClose || this.opts.canClose();
|
|
1655
|
+
this.release();
|
|
1656
|
+
if ((far || flick) && allowed) {
|
|
1657
|
+
if (sheet) sheet.style.transform = "translateY(100%)";
|
|
1658
|
+
this.opts.close();
|
|
1659
|
+
} else if (sheet) {
|
|
1660
|
+
sheet.style.transform = "";
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
this.onPointerCancel = (e) => {
|
|
1664
|
+
if (!this.drag || e.pointerId !== this.drag.pointerId) return;
|
|
1665
|
+
this.abandon();
|
|
1666
|
+
};
|
|
1667
|
+
/** The press that ended a drag also fires a click on ✕ or the grab bar: swallow it. */
|
|
1668
|
+
this.onClickCapture = (e) => {
|
|
1669
|
+
if (performance.now() - this.dragEndedAt > CLICK_AFTER_DRAG_MS) return;
|
|
1670
|
+
this.dragEndedAt = -Infinity;
|
|
1671
|
+
e.stopPropagation();
|
|
1672
|
+
e.preventDefault();
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
/** Call from `updated()` when `open` changed. Same value twice is a no-op. */
|
|
1676
|
+
update(open) {
|
|
1677
|
+
if (open === this.active) return;
|
|
1678
|
+
this.active = open;
|
|
1679
|
+
if (open) this.opened();
|
|
1680
|
+
else this.closed();
|
|
1681
|
+
}
|
|
1682
|
+
/** Call from `disconnectedCallback()`: a sheet torn down while open must not keep the page locked. */
|
|
1683
|
+
disconnect() {
|
|
1684
|
+
if (this.active) {
|
|
1685
|
+
this.active = false;
|
|
1686
|
+
this.closed();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
opened() {
|
|
1690
|
+
if (isPhoneSheet()) {
|
|
1691
|
+
lockPageScroll();
|
|
1692
|
+
this.locked = true;
|
|
1693
|
+
}
|
|
1694
|
+
this.opts.host.addEventListener("pointerdown", this.onPointerDown);
|
|
1695
|
+
this.opts.host.addEventListener("click", this.onClickCapture, true);
|
|
1696
|
+
}
|
|
1697
|
+
closed() {
|
|
1698
|
+
this.opts.host.removeEventListener("pointerdown", this.onPointerDown);
|
|
1699
|
+
this.opts.host.removeEventListener("click", this.onClickCapture, true);
|
|
1700
|
+
this.stopTracking();
|
|
1701
|
+
this.drag = null;
|
|
1702
|
+
const sheet = this.opts.sheet();
|
|
1703
|
+
if (sheet) {
|
|
1704
|
+
sheet.style.transform = "";
|
|
1705
|
+
sheet.style.transition = "";
|
|
1706
|
+
}
|
|
1707
|
+
const backdrop = this.opts.backdrop?.();
|
|
1708
|
+
if (backdrop) {
|
|
1709
|
+
backdrop.style.opacity = "";
|
|
1710
|
+
backdrop.style.transition = "";
|
|
1711
|
+
}
|
|
1712
|
+
if (this.locked) {
|
|
1713
|
+
this.locked = false;
|
|
1714
|
+
unlockPageScroll();
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/** Give up on this press and put the sheet back where it was. */
|
|
1718
|
+
abandon() {
|
|
1719
|
+
const moving = this.drag?.moving;
|
|
1720
|
+
this.stopTracking();
|
|
1721
|
+
this.drag = null;
|
|
1722
|
+
if (!moving) return;
|
|
1723
|
+
this.release();
|
|
1724
|
+
const sheet = this.opts.sheet();
|
|
1725
|
+
if (sheet) sheet.style.transform = "";
|
|
1726
|
+
}
|
|
1727
|
+
/** Hand the sheet and backdrop back to their CSS transitions. */
|
|
1728
|
+
release() {
|
|
1729
|
+
const sheet = this.opts.sheet();
|
|
1730
|
+
if (sheet) sheet.style.transition = "";
|
|
1731
|
+
const backdrop = this.opts.backdrop?.();
|
|
1732
|
+
if (backdrop) {
|
|
1733
|
+
backdrop.style.transition = "";
|
|
1734
|
+
backdrop.style.opacity = "";
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
stopTracking() {
|
|
1738
|
+
window.removeEventListener("pointermove", this.onPointerMove);
|
|
1739
|
+
window.removeEventListener("pointerup", this.onPointerUp);
|
|
1740
|
+
window.removeEventListener("pointercancel", this.onPointerCancel);
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
function inZone(e, sheet, zones) {
|
|
1744
|
+
for (const node of e.composedPath()) {
|
|
1745
|
+
if (node === sheet) return false;
|
|
1746
|
+
if (node instanceof Element && node.matches(zones)) return sheet.contains(node);
|
|
1747
|
+
}
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
var locks = 0;
|
|
1751
|
+
var saved = null;
|
|
1752
|
+
var LOCKED = ["position", "top", "left", "right", "width", "overflow"];
|
|
1753
|
+
function lockPageScroll() {
|
|
1754
|
+
if (typeof document === "undefined") return;
|
|
1755
|
+
locks += 1;
|
|
1756
|
+
if (locks > 1) return;
|
|
1757
|
+
const body = document.body;
|
|
1758
|
+
const scrollY = window.scrollY || document.documentElement.scrollTop || 0;
|
|
1759
|
+
saved = { scrollY, body: {} };
|
|
1760
|
+
for (const p of LOCKED) saved.body[p] = body.style[p];
|
|
1761
|
+
body.style.position = "fixed";
|
|
1762
|
+
body.style.top = `-${scrollY}px`;
|
|
1763
|
+
body.style.left = "0";
|
|
1764
|
+
body.style.right = "0";
|
|
1765
|
+
body.style.width = "100%";
|
|
1766
|
+
body.style.overflow = "hidden";
|
|
1767
|
+
}
|
|
1768
|
+
function unlockPageScroll() {
|
|
1769
|
+
if (typeof document === "undefined" || locks === 0) return;
|
|
1770
|
+
locks -= 1;
|
|
1771
|
+
if (locks > 0 || !saved) return;
|
|
1772
|
+
const { scrollY, body } = saved;
|
|
1773
|
+
saved = null;
|
|
1774
|
+
for (const p of LOCKED) document.body.style[p] = body[p] ?? "";
|
|
1775
|
+
const root = document.documentElement;
|
|
1776
|
+
const behavior = root.style.scrollBehavior;
|
|
1777
|
+
root.style.scrollBehavior = "auto";
|
|
1778
|
+
window.scrollTo(0, scrollY);
|
|
1779
|
+
root.style.scrollBehavior = behavior;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1454
1782
|
// src/ui/components/vio-product-detail.ts
|
|
1455
1783
|
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1456
1784
|
'<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>'
|
|
@@ -1485,6 +1813,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
1485
1813
|
this.selectedOptions = {};
|
|
1486
1814
|
this.quantity = 1;
|
|
1487
1815
|
this.activeImageIndex = 0;
|
|
1816
|
+
this.thumbsOverflow = false;
|
|
1817
|
+
this.thumbsAtStart = true;
|
|
1818
|
+
this.thumbsAtEnd = false;
|
|
1819
|
+
this.logoSquare = false;
|
|
1488
1820
|
this.adding = false;
|
|
1489
1821
|
this.applePayOk = false;
|
|
1490
1822
|
this.availableMethods = null;
|
|
@@ -1496,12 +1828,24 @@ var VioProductDetail = class extends LitElement {
|
|
|
1496
1828
|
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
1497
1829
|
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
1498
1830
|
this.applePayHandle = null;
|
|
1831
|
+
/** The amount `applePayHandle`'s sheet shows (product × quantity when it was prepared). */
|
|
1832
|
+
this.applePayAmount = null;
|
|
1833
|
+
/** The amount the latest preparation was asked for — a newer one supersedes it. */
|
|
1834
|
+
this.applePayAskedFor = null;
|
|
1499
1835
|
/** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
|
|
1500
1836
|
this.dialogFocus = new DialogFocus({
|
|
1501
1837
|
host: this,
|
|
1502
1838
|
container: () => this.renderRoot.querySelector(".modal"),
|
|
1503
1839
|
close: () => this.close()
|
|
1504
1840
|
});
|
|
1841
|
+
/** Phones: the page behind is locked, and the sheet closes by pulling the bar or the top bar down. */
|
|
1842
|
+
this.bottomSheet = new BottomSheet({
|
|
1843
|
+
host: this,
|
|
1844
|
+
sheet: () => this.renderRoot.querySelector(".modal"),
|
|
1845
|
+
backdrop: () => this.renderRoot.querySelector(".backdrop"),
|
|
1846
|
+
dragZones: ".handle, .topbar",
|
|
1847
|
+
close: () => this.close()
|
|
1848
|
+
});
|
|
1505
1849
|
this.boundProductClick = (e) => {
|
|
1506
1850
|
if (!this.autoOpen) return;
|
|
1507
1851
|
const ev = e;
|
|
@@ -1529,6 +1873,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
1529
1873
|
this.show();
|
|
1530
1874
|
void this.fetchProduct();
|
|
1531
1875
|
};
|
|
1876
|
+
this.onThumbsScroll = () => {
|
|
1877
|
+
const row = this.thumbRow();
|
|
1878
|
+
if (!row) return;
|
|
1879
|
+
const atStart = row.scrollLeft <= 1;
|
|
1880
|
+
const atEnd = row.scrollLeft + row.clientWidth >= row.scrollWidth - 1;
|
|
1881
|
+
if (atStart !== this.thumbsAtStart) this.thumbsAtStart = atStart;
|
|
1882
|
+
if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
|
|
1883
|
+
};
|
|
1532
1884
|
}
|
|
1533
1885
|
/** The card's context applies to the product it opened — not to one the host opened by hand later. */
|
|
1534
1886
|
get fromCard() {
|
|
@@ -1616,7 +1968,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
1616
1968
|
height: 92vh;
|
|
1617
1969
|
border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
|
|
1618
1970
|
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
|
|
1971
|
+
overscroll-behavior: contain;
|
|
1619
1972
|
}
|
|
1973
|
+
/* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
|
|
1974
|
+
.handle, .topbar { touch-action: none; }
|
|
1620
1975
|
.handle {
|
|
1621
1976
|
display: flex;
|
|
1622
1977
|
justify-content: center;
|
|
@@ -1637,8 +1992,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
1637
1992
|
@media (min-width: 601px) {
|
|
1638
1993
|
.modal {
|
|
1639
1994
|
inset: 0 0 0 auto;
|
|
1640
|
-
width: min(440px, 100%);
|
|
1641
|
-
max-width: 440px;
|
|
1995
|
+
width: min(var(--vio-detail-width, 440px), 100%);
|
|
1996
|
+
max-width: var(--vio-detail-width, 440px);
|
|
1642
1997
|
border-radius: 0;
|
|
1643
1998
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
1644
1999
|
transform: translateX(100%);
|
|
@@ -1652,29 +2007,59 @@ var VioProductDetail = class extends LitElement {
|
|
|
1652
2007
|
max-width: none;
|
|
1653
2008
|
grid-template-columns: 1fr;
|
|
1654
2009
|
gap: 20px;
|
|
1655
|
-
padding:
|
|
2010
|
+
padding: var(--vio-detail-padding-top, 0px) var(--vio-detail-padding-x, var(--vio-space-lg, 24px)) 40px;
|
|
1656
2011
|
}
|
|
1657
|
-
.modal .name { font-size: 26px; line-height: 1.2; }
|
|
1658
|
-
.modal .gallery-main { width:
|
|
2012
|
+
.modal .name { font-size: var(--vio-detail-title-size, 26px); line-height: 1.2; }
|
|
2013
|
+
.modal .gallery-main { width: auto; }
|
|
1659
2014
|
}
|
|
1660
2015
|
|
|
1661
2016
|
.topbar {
|
|
1662
2017
|
display: flex;
|
|
1663
2018
|
justify-content: space-between;
|
|
1664
2019
|
align-items: center;
|
|
1665
|
-
|
|
2020
|
+
gap: 16px;
|
|
2021
|
+
min-height: var(--vio-detail-topbar-height, 64px);
|
|
2022
|
+
box-sizing: border-box;
|
|
2023
|
+
padding: 10px var(--vio-detail-padding-x, var(--vio-space-lg, 24px));
|
|
1666
2024
|
position: sticky;
|
|
1667
2025
|
top: 0;
|
|
1668
2026
|
background: var(--vio-color-surface, #fff);
|
|
1669
2027
|
border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
|
|
1670
2028
|
z-index: 1;
|
|
1671
2029
|
}
|
|
1672
|
-
.
|
|
1673
|
-
|
|
2030
|
+
/* Who sells it, top left beside the close: the sponsor's logo, else its name. */
|
|
2031
|
+
.topbar-sponsor { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2032
|
+
/* The logo box clips, so --vio-detail-logo-zoom can crop the white a
|
|
2033
|
+
square file carries around its mark. */
|
|
2034
|
+
.topbar-logo-box {
|
|
2035
|
+
flex: 0 0 auto;
|
|
2036
|
+
display: flex;
|
|
2037
|
+
align-items: center;
|
|
2038
|
+
height: var(--vio-detail-logo-height, 44px);
|
|
2039
|
+
max-width: var(--vio-detail-logo-max-width, 220px);
|
|
2040
|
+
overflow: hidden;
|
|
2041
|
+
}
|
|
2042
|
+
.topbar-logo {
|
|
2043
|
+
display: block;
|
|
2044
|
+
width: auto;
|
|
2045
|
+
height: 100%;
|
|
2046
|
+
max-width: var(--vio-detail-logo-max-width, 220px);
|
|
2047
|
+
object-fit: contain;
|
|
2048
|
+
object-position: left center;
|
|
2049
|
+
transform: scale(var(--vio-detail-logo-zoom, 1));
|
|
2050
|
+
transform-origin: center;
|
|
2051
|
+
}
|
|
2052
|
+
/* A square (or near) logo is a mark, not a wordmark: say whose it is. */
|
|
2053
|
+
.topbar-sponsor:not(.is-square) .topbar-mark-name { display: none; }
|
|
2054
|
+
.topbar-name {
|
|
2055
|
+
font-size: var(--vio-detail-logo-name-size, 13px);
|
|
1674
2056
|
text-transform: uppercase;
|
|
1675
2057
|
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
1676
2058
|
color: var(--vio-color-text-secondary, #666);
|
|
1677
|
-
font-weight:
|
|
2059
|
+
font-weight: 600;
|
|
2060
|
+
white-space: nowrap;
|
|
2061
|
+
overflow: hidden;
|
|
2062
|
+
text-overflow: ellipsis;
|
|
1678
2063
|
}
|
|
1679
2064
|
.close {
|
|
1680
2065
|
background: none;
|
|
@@ -1700,74 +2085,110 @@ var VioProductDetail = class extends LitElement {
|
|
|
1700
2085
|
gap: 64px;
|
|
1701
2086
|
}
|
|
1702
2087
|
@media (max-width: 800px) {
|
|
1703
|
-
.body {
|
|
2088
|
+
.body {
|
|
2089
|
+
grid-template-columns: 1fr;
|
|
2090
|
+
gap: 24px;
|
|
2091
|
+
padding: var(--vio-detail-padding-top, 0px) var(--vio-detail-padding-x, var(--vio-space-lg, 24px)) 64px;
|
|
2092
|
+
}
|
|
1704
2093
|
}
|
|
1705
2094
|
|
|
1706
|
-
/* Gallery
|
|
1707
|
-
|
|
2095
|
+
/* Gallery. One fixed proportion for every image (4:5 by default) that the
|
|
2096
|
+
image fills (cover): no white bars, and the panel never jumps between
|
|
2097
|
+
photos of different shapes (Angelo, 2026-10-22). Its side margin is
|
|
2098
|
+
--vio-detail-image-inset (the panel's padding by default; 0 = edge to
|
|
2099
|
+
edge). Fit, proportion, a height cap and a zoom (to crop a white margin
|
|
2100
|
+
baked into the file) are the host's to set. */
|
|
2101
|
+
.gallery { display: flex; flex-direction: column; gap: var(--vio-detail-gallery-gap, 12px); min-width: 0; }
|
|
1708
2102
|
.gallery-main {
|
|
1709
|
-
|
|
1710
|
-
|
|
2103
|
+
position: relative;
|
|
2104
|
+
margin-inline: calc(
|
|
2105
|
+
var(--vio-detail-image-inset, var(--vio-detail-padding-x, var(--vio-space-lg, 24px))) -
|
|
2106
|
+
var(--vio-detail-padding-x, var(--vio-space-lg, 24px))
|
|
2107
|
+
);
|
|
2108
|
+
aspect-ratio: var(--vio-detail-image-aspect, 4 / 5);
|
|
2109
|
+
max-height: var(--vio-detail-image-max-height, 85vh);
|
|
2110
|
+
background: var(--vio-detail-image-bg, var(--vio-color-surface-muted, #f2f2f2));
|
|
2111
|
+
border-radius: var(--vio-detail-image-radius, 0);
|
|
1711
2112
|
display: flex;
|
|
1712
2113
|
align-items: center;
|
|
1713
2114
|
justify-content: center;
|
|
1714
2115
|
overflow: hidden;
|
|
1715
2116
|
}
|
|
1716
2117
|
.gallery-main img {
|
|
2118
|
+
display: block;
|
|
1717
2119
|
width: 100%;
|
|
1718
2120
|
height: 100%;
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2121
|
+
max-height: var(--vio-detail-image-max-height, 85vh);
|
|
2122
|
+
object-fit: var(--vio-detail-image-fit, cover);
|
|
2123
|
+
object-position: var(--vio-detail-image-position, center);
|
|
2124
|
+
transform: scale(var(--vio-detail-image-zoom, 1));
|
|
2125
|
+
}
|
|
2126
|
+
/* Thumbnails: one row, never a second line — swipe on touch, arrows with a
|
|
2127
|
+
mouse; the selected one scrolls into view by itself. */
|
|
2128
|
+
.thumbs { position: relative; min-width: 0; }
|
|
1723
2129
|
.gallery-thumbs {
|
|
1724
2130
|
display: flex;
|
|
1725
|
-
|
|
1726
|
-
|
|
2131
|
+
flex-wrap: nowrap;
|
|
2132
|
+
gap: var(--vio-detail-thumb-gap, 8px);
|
|
2133
|
+
overflow-x: auto;
|
|
2134
|
+
scroll-snap-type: x proximity;
|
|
2135
|
+
scroll-behavior: smooth;
|
|
2136
|
+
scrollbar-width: none;
|
|
2137
|
+
-webkit-overflow-scrolling: touch;
|
|
1727
2138
|
}
|
|
2139
|
+
.gallery-thumbs::-webkit-scrollbar { display: none; }
|
|
1728
2140
|
.gallery-thumb {
|
|
1729
|
-
|
|
1730
|
-
|
|
2141
|
+
flex: 0 0 auto;
|
|
2142
|
+
width: var(--vio-detail-thumb-size, 64px);
|
|
2143
|
+
height: var(--vio-detail-thumb-size, 64px);
|
|
1731
2144
|
background: var(--vio-color-surface-muted, #f2f2f2);
|
|
1732
|
-
border: 1px solid transparent;
|
|
2145
|
+
border: var(--vio-detail-thumb-border-width, 1px) solid transparent;
|
|
2146
|
+
border-radius: var(--vio-detail-thumb-radius, 0);
|
|
1733
2147
|
cursor: pointer;
|
|
1734
2148
|
padding: 0;
|
|
1735
2149
|
overflow: hidden;
|
|
2150
|
+
scroll-snap-align: start;
|
|
2151
|
+
box-sizing: border-box;
|
|
2152
|
+
opacity: var(--vio-detail-thumb-opacity, 1);
|
|
1736
2153
|
}
|
|
1737
2154
|
.gallery-thumb img {
|
|
2155
|
+
display: block;
|
|
1738
2156
|
width: 100%;
|
|
1739
2157
|
height: 100%;
|
|
1740
|
-
object-fit:
|
|
1741
|
-
padding: 6px;
|
|
1742
|
-
box-sizing: border-box;
|
|
2158
|
+
object-fit: var(--vio-detail-thumb-fit, cover);
|
|
1743
2159
|
}
|
|
1744
|
-
.gallery-thumb.active {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
.sponsor-header {
|
|
1748
|
-
grid-column: 1 / -1;
|
|
1749
|
-
display: flex;
|
|
1750
|
-
align-items: center;
|
|
1751
|
-
min-height: 24px;
|
|
2160
|
+
.gallery-thumb.active {
|
|
2161
|
+
border-color: var(--vio-detail-thumb-active-color, var(--vio-color-text, #0a0a0a));
|
|
2162
|
+
opacity: 1;
|
|
1752
2163
|
}
|
|
1753
|
-
.
|
|
1754
|
-
display:
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2164
|
+
.thumbs-arrow {
|
|
2165
|
+
display: none;
|
|
2166
|
+
position: absolute;
|
|
2167
|
+
top: 50%;
|
|
2168
|
+
transform: translateY(-50%);
|
|
2169
|
+
z-index: 1;
|
|
2170
|
+
width: 28px;
|
|
2171
|
+
height: 28px;
|
|
2172
|
+
border-radius: 50%;
|
|
2173
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2174
|
+
background: var(--vio-color-surface, #fff);
|
|
2175
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
2176
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
2177
|
+
cursor: pointer;
|
|
2178
|
+
align-items: center;
|
|
2179
|
+
justify-content: center;
|
|
2180
|
+
padding: 0;
|
|
2181
|
+
font: 16px/1 sans-serif;
|
|
1760
2182
|
}
|
|
1761
|
-
.
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
font-weight: 600;
|
|
2183
|
+
.thumbs-arrow.prev { left: -6px; }
|
|
2184
|
+
.thumbs-arrow.next { right: -6px; }
|
|
2185
|
+
.thumbs-arrow:disabled { visibility: hidden; }
|
|
2186
|
+
@media (hover: hover) and (pointer: fine) {
|
|
2187
|
+
.thumbs.overflows .thumbs-arrow { display: flex; }
|
|
1767
2188
|
}
|
|
1768
2189
|
|
|
1769
2190
|
/* Info column */
|
|
1770
|
-
.info { display: flex; flex-direction: column; gap:
|
|
2191
|
+
.info { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); min-width: 0; }
|
|
1771
2192
|
.brand {
|
|
1772
2193
|
font-size: 12px;
|
|
1773
2194
|
text-transform: uppercase;
|
|
@@ -1777,8 +2198,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
1777
2198
|
}
|
|
1778
2199
|
.name {
|
|
1779
2200
|
font-family: var(--vio-font-serif, Georgia, serif);
|
|
1780
|
-
font-size: clamp(28px, 4vw, 40px);
|
|
1781
|
-
font-weight: 400;
|
|
2201
|
+
font-size: var(--vio-detail-title-size, clamp(28px, 4vw, 40px));
|
|
2202
|
+
font-weight: var(--vio-detail-title-weight, 400);
|
|
1782
2203
|
line-height: 1.15;
|
|
1783
2204
|
margin: 0;
|
|
1784
2205
|
letter-spacing: -0.01em;
|
|
@@ -1789,8 +2210,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
1789
2210
|
gap: 16px;
|
|
1790
2211
|
}
|
|
1791
2212
|
.price {
|
|
1792
|
-
font-size: 24px;
|
|
1793
|
-
font-weight: 700;
|
|
2213
|
+
font-size: var(--vio-detail-price-size, 24px);
|
|
2214
|
+
font-weight: var(--vio-detail-price-weight, 700);
|
|
1794
2215
|
}
|
|
1795
2216
|
.compare-at {
|
|
1796
2217
|
font-size: 16px;
|
|
@@ -1808,7 +2229,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
1808
2229
|
.stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
|
|
1809
2230
|
|
|
1810
2231
|
/* Option pickers */
|
|
1811
|
-
.option-group { display: flex; flex-direction: column; gap:
|
|
2232
|
+
.option-group { display: flex; flex-direction: column; gap: 6px; }
|
|
1812
2233
|
.option-label {
|
|
1813
2234
|
font-size: 11px;
|
|
1814
2235
|
text-transform: uppercase;
|
|
@@ -1816,14 +2237,28 @@ var VioProductDetail = class extends LitElement {
|
|
|
1816
2237
|
color: var(--vio-color-text-secondary, #666);
|
|
1817
2238
|
font-weight: 500;
|
|
1818
2239
|
}
|
|
1819
|
-
|
|
2240
|
+
/* Sizes: compact, still comfortable to tap. wrap → several lines;
|
|
2241
|
+
nowrap → one line that scrolls sideways. */
|
|
2242
|
+
.option-values {
|
|
2243
|
+
display: flex;
|
|
2244
|
+
flex-wrap: var(--vio-detail-option-wrap, wrap);
|
|
2245
|
+
gap: var(--vio-detail-option-gap, 6px);
|
|
2246
|
+
overflow-x: auto;
|
|
2247
|
+
scrollbar-width: none;
|
|
2248
|
+
}
|
|
2249
|
+
.option-values::-webkit-scrollbar { display: none; }
|
|
1820
2250
|
.option-pill {
|
|
1821
|
-
|
|
2251
|
+
flex: 0 0 auto;
|
|
2252
|
+
box-sizing: border-box;
|
|
2253
|
+
min-height: var(--vio-detail-option-height, 36px);
|
|
2254
|
+
min-width: var(--vio-detail-option-height, 36px);
|
|
2255
|
+
padding: 0 var(--vio-detail-option-padding-x, 12px);
|
|
2256
|
+
border-radius: var(--vio-detail-option-radius, 0);
|
|
1822
2257
|
background: #fff;
|
|
1823
2258
|
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
1824
2259
|
cursor: pointer;
|
|
1825
2260
|
font-family: inherit;
|
|
1826
|
-
font-size: 13px;
|
|
2261
|
+
font-size: var(--vio-detail-option-font-size, 13px);
|
|
1827
2262
|
font-weight: 500;
|
|
1828
2263
|
color: inherit;
|
|
1829
2264
|
transition: border-color 0.15s, background 0.15s;
|
|
@@ -1842,6 +2277,34 @@ var VioProductDetail = class extends LitElement {
|
|
|
1842
2277
|
}
|
|
1843
2278
|
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
1844
2279
|
|
|
2280
|
+
/* The buy zone: Vipps first, then quantity + add to cart, then the other
|
|
2281
|
+
ways to pay. --vio-detail-buy-position: sticky pins it to the bottom of
|
|
2282
|
+
the panel while the shopper scrolls (mobile, mostly). */
|
|
2283
|
+
.buy {
|
|
2284
|
+
display: flex;
|
|
2285
|
+
flex-direction: column;
|
|
2286
|
+
gap: var(--vio-detail-buy-gap, 10px);
|
|
2287
|
+
position: var(--vio-detail-buy-position, static);
|
|
2288
|
+
bottom: 0;
|
|
2289
|
+
z-index: 2;
|
|
2290
|
+
background: var(--vio-detail-buy-bg, var(--vio-color-surface, #fff));
|
|
2291
|
+
padding: var(--vio-detail-buy-padding, 0);
|
|
2292
|
+
box-shadow: var(--vio-detail-buy-shadow, none);
|
|
2293
|
+
}
|
|
2294
|
+
.buy > * { margin-top: 0 !important; }
|
|
2295
|
+
/* Under the buttons: who the shopper is buying from. */
|
|
2296
|
+
.seller-note {
|
|
2297
|
+
margin: 2px 0 0;
|
|
2298
|
+
font-size: var(--vio-detail-seller-note-size, 12px);
|
|
2299
|
+
line-height: 1.4;
|
|
2300
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2301
|
+
text-align: var(--vio-detail-seller-note-align, center);
|
|
2302
|
+
}
|
|
2303
|
+
.seller-note strong { font-weight: 600; color: var(--vio-color-text, #0a0a0a); }
|
|
2304
|
+
.seller-links { display: block; margin-top: 2px; }
|
|
2305
|
+
.seller-links a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
2306
|
+
.seller-links a + a::before { content: '·'; display: inline-block; margin: 0 6px; text-decoration: none; }
|
|
2307
|
+
|
|
1845
2308
|
/* Qty + CTA row */
|
|
1846
2309
|
.qty-cta-row {
|
|
1847
2310
|
display: grid;
|
|
@@ -1856,7 +2319,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
1856
2319
|
}
|
|
1857
2320
|
.qty-btn {
|
|
1858
2321
|
width: 44px;
|
|
1859
|
-
height:
|
|
2322
|
+
height: var(--vio-detail-button-height, 52px);
|
|
1860
2323
|
background: #fff;
|
|
1861
2324
|
border: none;
|
|
1862
2325
|
cursor: pointer;
|
|
@@ -1874,7 +2337,9 @@ var VioProductDetail = class extends LitElement {
|
|
|
1874
2337
|
background: var(--vio-color-text, #0a0a0a);
|
|
1875
2338
|
color: var(--vio-color-text-on-primary, #fff);
|
|
1876
2339
|
border: none;
|
|
1877
|
-
|
|
2340
|
+
min-height: var(--vio-detail-button-height, 52px);
|
|
2341
|
+
border-radius: var(--vio-detail-button-radius, 0);
|
|
2342
|
+
padding: 0 24px;
|
|
1878
2343
|
font-size: 12px;
|
|
1879
2344
|
text-transform: uppercase;
|
|
1880
2345
|
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
@@ -1984,19 +2449,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
1984
2449
|
.buy-applepay:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1985
2450
|
|
|
1986
2451
|
/* Description */
|
|
2452
|
+
/* The description folds: a tap opens it, so it does not take the page. */
|
|
2453
|
+
.description-block { border-top: 1px solid var(--vio-color-border, #e5e5e5); }
|
|
2454
|
+
.description-block > summary {
|
|
2455
|
+
list-style: none;
|
|
2456
|
+
cursor: pointer;
|
|
2457
|
+
display: flex;
|
|
2458
|
+
align-items: center;
|
|
2459
|
+
justify-content: space-between;
|
|
2460
|
+
padding: 14px 0;
|
|
2461
|
+
font-size: 11px;
|
|
2462
|
+
text-transform: uppercase;
|
|
2463
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
2464
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2465
|
+
font-weight: 600;
|
|
2466
|
+
}
|
|
2467
|
+
.description-block > summary::-webkit-details-marker { display: none; }
|
|
2468
|
+
.description-block > summary::after {
|
|
2469
|
+
content: '';
|
|
2470
|
+
width: 8px;
|
|
2471
|
+
height: 8px;
|
|
2472
|
+
border-right: 1.5px solid currentColor;
|
|
2473
|
+
border-bottom: 1.5px solid currentColor;
|
|
2474
|
+
transform: rotate(45deg) translateY(-2px);
|
|
2475
|
+
transition: transform 0.15s ease;
|
|
2476
|
+
}
|
|
2477
|
+
.description-block[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
|
|
1987
2478
|
.description {
|
|
1988
2479
|
font-size: 14px;
|
|
1989
2480
|
line-height: 1.6;
|
|
1990
2481
|
color: var(--vio-color-text-secondary, #444);
|
|
1991
|
-
|
|
2482
|
+
padding-bottom: 14px;
|
|
2483
|
+
white-space: pre-line;
|
|
1992
2484
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2485
|
+
/* Desktop: the description above, the buy zone at the bottom of the panel
|
|
2486
|
+
(sticky). Phones keep the order as it is: buy, then the description. */
|
|
2487
|
+
@media (min-width: 601px) {
|
|
2488
|
+
.description-block { order: 1; }
|
|
2489
|
+
.buy {
|
|
2490
|
+
order: 2;
|
|
2491
|
+
position: var(--vio-detail-buy-position, sticky);
|
|
2492
|
+
padding: var(--vio-detail-buy-padding, 12px 0 16px);
|
|
2493
|
+
box-shadow: var(--vio-detail-buy-shadow, 0 -1px 0 var(--vio-color-border, #e5e5e5));
|
|
2494
|
+
}
|
|
2000
2495
|
}
|
|
2001
2496
|
|
|
2002
2497
|
/* Skeleton loader: shimmer placeholders mirroring the product layout
|
|
@@ -2052,14 +2547,50 @@ var VioProductDetail = class extends LitElement {
|
|
|
2052
2547
|
disconnectedCallback() {
|
|
2053
2548
|
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
2054
2549
|
this.slotRegistration.clear();
|
|
2550
|
+
this.bottomSheet.disconnect();
|
|
2055
2551
|
super.disconnectedCallback();
|
|
2056
2552
|
}
|
|
2057
2553
|
updated(changed) {
|
|
2058
|
-
if (changed.has("open"))
|
|
2554
|
+
if (changed.has("open")) {
|
|
2555
|
+
this.dialogFocus.update(this.open);
|
|
2556
|
+
this.bottomSheet.update(this.open);
|
|
2557
|
+
}
|
|
2558
|
+
if (changed.has("product") || changed.has("open")) this.measureThumbs();
|
|
2559
|
+
if ((changed.has("quantity") || changed.has("selectedOptions")) && this.product && this.applePayAskedFor !== null) {
|
|
2560
|
+
if (this.unitPrice * this.quantity !== this.applePayAskedFor) this.prepareApplePaySheet();
|
|
2561
|
+
}
|
|
2562
|
+
if (changed.has("activeImageIndex")) this.revealActiveThumb();
|
|
2563
|
+
this.measureLogo();
|
|
2059
2564
|
if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
|
|
2060
2565
|
this.slotRegistration.sync(this.slotAttributes());
|
|
2061
2566
|
}
|
|
2062
2567
|
}
|
|
2568
|
+
thumbRow() {
|
|
2569
|
+
return this.renderRoot.querySelector(".gallery-thumbs");
|
|
2570
|
+
}
|
|
2571
|
+
/** Arrows only when the row overflows; each one off at its own end. */
|
|
2572
|
+
measureThumbs() {
|
|
2573
|
+
const row = this.thumbRow();
|
|
2574
|
+
if (!row) return;
|
|
2575
|
+
const overflow = row.scrollWidth > row.clientWidth + 1;
|
|
2576
|
+
if (overflow !== this.thumbsOverflow) this.thumbsOverflow = overflow;
|
|
2577
|
+
this.onThumbsScroll();
|
|
2578
|
+
}
|
|
2579
|
+
/** One arrow press moves most of a visible row. */
|
|
2580
|
+
scrollThumbs(direction) {
|
|
2581
|
+
const row = this.thumbRow();
|
|
2582
|
+
if (row) row.scrollBy({ left: direction * row.clientWidth * 0.8 });
|
|
2583
|
+
}
|
|
2584
|
+
/** The selected thumbnail is always visible: the row scrolls to it, the page never does. */
|
|
2585
|
+
revealActiveThumb() {
|
|
2586
|
+
const row = this.thumbRow();
|
|
2587
|
+
const thumb = row?.querySelectorAll(".gallery-thumb")[this.activeImageIndex];
|
|
2588
|
+
if (!row || !thumb) return;
|
|
2589
|
+
const left = thumb.offsetLeft - row.offsetLeft;
|
|
2590
|
+
if (left < row.scrollLeft || left + thumb.offsetWidth > row.scrollLeft + row.clientWidth) {
|
|
2591
|
+
row.scrollTo({ left: Math.max(0, left - (row.clientWidth - thumb.offsetWidth) / 2) });
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2063
2594
|
show() {
|
|
2064
2595
|
this.open = true;
|
|
2065
2596
|
}
|
|
@@ -2157,13 +2688,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2157
2688
|
if (values.length > 0) initial[opt.name] = values[0];
|
|
2158
2689
|
}
|
|
2159
2690
|
this.selectedOptions = initial;
|
|
2160
|
-
|
|
2161
|
-
this.applePayHandle = handle;
|
|
2162
|
-
this.applePayOk = handle.available;
|
|
2163
|
-
}).catch(() => {
|
|
2164
|
-
this.applePayHandle = null;
|
|
2165
|
-
this.applePayOk = false;
|
|
2166
|
-
});
|
|
2691
|
+
this.prepareApplePaySheet();
|
|
2167
2692
|
void this.loadAvailablePaymentMethods();
|
|
2168
2693
|
}
|
|
2169
2694
|
this.isLoading = false;
|
|
@@ -2546,27 +3071,47 @@ var VioProductDetail = class extends LitElement {
|
|
|
2546
3071
|
}
|
|
2547
3072
|
buyWithVipps() {
|
|
2548
3073
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3074
|
+
if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
|
|
3075
|
+
this.addCurrentToCart();
|
|
3076
|
+
this.close();
|
|
3077
|
+
this.openCheckoutWithVipps();
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.sponsorId, source: "detail" });
|
|
2549
3081
|
this.addCurrentToCart();
|
|
2550
3082
|
this.close();
|
|
2551
|
-
void this.startVippsExpressOrCheckout();
|
|
3083
|
+
void this.startVippsExpressOrCheckout(redirect);
|
|
2552
3084
|
}
|
|
2553
|
-
async startVippsExpressOrCheckout() {
|
|
3085
|
+
async startVippsExpressOrCheckout(redirect) {
|
|
2554
3086
|
let fallback = true;
|
|
2555
3087
|
try {
|
|
2556
|
-
if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId))
|
|
2557
|
-
|
|
2558
|
-
|
|
3088
|
+
if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId)) {
|
|
3089
|
+
redirect.abort("express-off");
|
|
3090
|
+
} else {
|
|
3091
|
+
const res = await Vio.checkout.startVippsExpress(this.sponsorId);
|
|
3092
|
+
fallback = !res?.payment_url;
|
|
3093
|
+
if (fallback) redirect.abort("not-started");
|
|
3094
|
+
}
|
|
2559
3095
|
} catch (err) {
|
|
2560
3096
|
if (typeof console !== "undefined") {
|
|
2561
3097
|
console.warn("[VioProductDetail] Vipps Express not started:", err);
|
|
2562
3098
|
}
|
|
3099
|
+
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
2563
3100
|
}
|
|
2564
3101
|
if (!fallback) return;
|
|
3102
|
+
this.openCheckoutWithVipps();
|
|
3103
|
+
}
|
|
3104
|
+
/** The ordinary way in, Vipps preselected: the host opens the checkout on `vio:checkout-open`. */
|
|
3105
|
+
openCheckoutWithVipps() {
|
|
3106
|
+
this.openCheckoutWith("vipps");
|
|
3107
|
+
}
|
|
3108
|
+
/** The ordinary way in with a method preselected: the host opens the checkout on `vio:checkout-open`. */
|
|
3109
|
+
openCheckoutWith(paymentMethod) {
|
|
2565
3110
|
this.dispatchEvent(
|
|
2566
3111
|
new CustomEvent("vio:checkout-open", {
|
|
2567
3112
|
bubbles: true,
|
|
2568
3113
|
composed: true,
|
|
2569
|
-
detail: { sponsorId: this.sponsorId, paymentMethod
|
|
3114
|
+
detail: { sponsorId: this.sponsorId, paymentMethod, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
|
|
2570
3115
|
})
|
|
2571
3116
|
);
|
|
2572
3117
|
}
|
|
@@ -2583,14 +3128,47 @@ var VioProductDetail = class extends LitElement {
|
|
|
2583
3128
|
})
|
|
2584
3129
|
);
|
|
2585
3130
|
}
|
|
3131
|
+
/** Prepare the Apple Pay sheet for this product × quantity; the newest preparation wins. */
|
|
3132
|
+
prepareApplePaySheet() {
|
|
3133
|
+
const amount = this.unitPrice * this.quantity;
|
|
3134
|
+
this.applePayAskedFor = amount;
|
|
3135
|
+
void Vio.checkout.prepareApplePayFor(amount, this.currency).then((handle) => {
|
|
3136
|
+
if (this.applePayAskedFor !== amount) return;
|
|
3137
|
+
this.applePayHandle = handle;
|
|
3138
|
+
this.applePayAmount = amount;
|
|
3139
|
+
this.applePayOk = handle.available;
|
|
3140
|
+
}).catch(() => {
|
|
3141
|
+
if (this.applePayAskedFor !== amount) return;
|
|
3142
|
+
this.applePayHandle = null;
|
|
3143
|
+
this.applePayAmount = null;
|
|
3144
|
+
this.applePayOk = false;
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
2586
3147
|
/**
|
|
2587
3148
|
* Express buy with Apple Pay. Runs synchronously inside the click so the
|
|
2588
3149
|
* Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
|
|
2589
3150
|
* state) → launch the sheet. On authorize, `payment-complete` drives the
|
|
2590
3151
|
* confirmation drawer (same path as the in-checkout Apple Pay button).
|
|
3152
|
+
*
|
|
3153
|
+
* The sheet's amount is fixed before the tap (Apple blocks a sheet opened
|
|
3154
|
+
* after an await, so it cannot wait for the server's cart), while the
|
|
3155
|
+
* charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
|
|
3156
|
+
* what will be charged: an empty cart, and a sheet prepared for this
|
|
3157
|
+
* product × quantity. Otherwise — lines already in the cart (as with Vipps,
|
|
3158
|
+
* see buyWithVipps), or a quantity whose sheet is still being prepared —
|
|
3159
|
+
* the product is added and the checkout opens with Apple Pay preselected:
|
|
3160
|
+
* every line and the total on screen, and its own Apple Pay button pays
|
|
3161
|
+
* that total.
|
|
2591
3162
|
*/
|
|
2592
3163
|
buyWithApplePay() {
|
|
2593
3164
|
if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
|
|
3165
|
+
const cartHasLines = (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
|
|
3166
|
+
if (cartHasLines || this.applePayAmount !== this.unitPrice * this.quantity) {
|
|
3167
|
+
this.addCurrentToCart();
|
|
3168
|
+
this.close();
|
|
3169
|
+
this.openCheckoutWith("apple-pay");
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
2594
3172
|
this.addCurrentToCart();
|
|
2595
3173
|
try {
|
|
2596
3174
|
Vio.checkout.open(this.sponsorId);
|
|
@@ -2620,28 +3198,67 @@ var VioProductDetail = class extends LitElement {
|
|
|
2620
3198
|
this.sellerSettled = true;
|
|
2621
3199
|
}
|
|
2622
3200
|
/**
|
|
2623
|
-
* Who sells the product
|
|
2624
|
-
*
|
|
2625
|
-
*
|
|
2626
|
-
*
|
|
2627
|
-
*
|
|
3201
|
+
* Who sells the product, top left beside the close (Mote & Livsstil,
|
|
3202
|
+
* 2026-10-22): the logo of the sponsor the product was fetched with — the
|
|
3203
|
+
* product's brand, never the article's — with its name as `alt`; without a
|
|
3204
|
+
* logo that loads, its name; without an identity, the seller's Company name
|
|
3205
|
+
* (or the product's brand) once known. Size: --vio-detail-logo-height.
|
|
2628
3206
|
*/
|
|
2629
|
-
|
|
3207
|
+
renderTopbarSponsor() {
|
|
2630
3208
|
const identity = resolveSponsorIdentity(this.sponsorId);
|
|
2631
|
-
|
|
2632
|
-
if (
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
3209
|
+
const mark = identity ? renderSponsorMark(this, identity, { logo: "topbar-logo", name: "topbar-name" }) : nothing;
|
|
3210
|
+
if (mark !== nothing) {
|
|
3211
|
+
if (identity && usableLogo(identity)) {
|
|
3212
|
+
return html`<span class="topbar-logo-box">${mark}</span
|
|
3213
|
+
><span class="topbar-name topbar-mark-name">${identity.name}</span>`;
|
|
3214
|
+
}
|
|
3215
|
+
return mark;
|
|
3216
|
+
}
|
|
3217
|
+
const name = this.headerName();
|
|
3218
|
+
return name ? html`<span class="topbar-name">${name}</span>` : nothing;
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Under the buy buttons: «Du handler direkte hos {brand}.» — the brand the
|
|
3222
|
+
* product was fetched with (the sponsor; its Company name or the product's
|
|
3223
|
+
* brand when no identity is known), never the article's. Nothing when no
|
|
3224
|
+
* name is known.
|
|
3225
|
+
*/
|
|
3226
|
+
renderSellerNote() {
|
|
3227
|
+
const identity = resolveSponsorIdentity(this.sponsorId);
|
|
3228
|
+
const name = identity?.name || this.headerName();
|
|
3229
|
+
if (!name) return nothing;
|
|
3230
|
+
const links = [
|
|
3231
|
+
identity?.termsUrl ? html`<a href=${identity.termsUrl} target="_blank" rel="noopener noreferrer" part="seller-terms">Kjøpsvilkår</a>` : null,
|
|
3232
|
+
identity?.refundPolicyUrl ? html`<a href=${identity.refundPolicyUrl} target="_blank" rel="noopener noreferrer" part="seller-refund">Angrerett og retur</a>` : null
|
|
3233
|
+
].filter(Boolean);
|
|
3234
|
+
return html`<p class="seller-note" part="seller-note">
|
|
3235
|
+
Du handler direkte hos <strong>${name}</strong>.${links.length ? html`<span class="seller-links">${links}</span>` : nothing}
|
|
3236
|
+
</p>`;
|
|
3237
|
+
}
|
|
3238
|
+
/** Square-ish logo (4:5 to 5:4) → the top bar shows the name beside it. */
|
|
3239
|
+
measureLogo() {
|
|
3240
|
+
const img = this.renderRoot.querySelector(".topbar-logo");
|
|
3241
|
+
if (!img) {
|
|
3242
|
+
if (this.logoSquare) this.logoSquare = false;
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
const decide = () => {
|
|
3246
|
+
if (!img.naturalWidth || !img.naturalHeight) return;
|
|
3247
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
3248
|
+
const square = ratio >= 0.8 && ratio <= 1.25;
|
|
3249
|
+
if (square !== this.logoSquare) this.logoSquare = square;
|
|
3250
|
+
};
|
|
3251
|
+
if (img.complete) decide();
|
|
3252
|
+
else img.addEventListener("load", decide, { once: true });
|
|
2636
3253
|
}
|
|
2637
3254
|
render() {
|
|
2638
3255
|
return html`
|
|
2639
3256
|
<div class="backdrop" @click=${this.close}></div>
|
|
2640
3257
|
<div class="modal" role="dialog" aria-modal="true" aria-label="Produkt" tabindex="-1">
|
|
2641
3258
|
<div class="handle" @click=${this.close} aria-hidden="true"></div>
|
|
2642
|
-
<div class="topbar">
|
|
2643
|
-
<
|
|
2644
|
-
<button class="close" @click=${this.close} aria-label="Lukk">×</button>
|
|
3259
|
+
<div class="topbar" part="topbar">
|
|
3260
|
+
<div class="topbar-sponsor ${this.logoSquare ? "is-square" : ""}" part="sponsor">${this.renderTopbarSponsor()}</div>
|
|
3261
|
+
<button class="close" part="close" @click=${this.close} aria-label="Lukk">×</button>
|
|
2645
3262
|
</div>
|
|
2646
3263
|
|
|
2647
3264
|
${this.renderBody()}
|
|
@@ -2673,45 +3290,90 @@ var VioProductDetail = class extends LitElement {
|
|
|
2673
3290
|
const activeImage = this.activeImage;
|
|
2674
3291
|
return html`
|
|
2675
3292
|
<div class="body">
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
<div class="gallery-main">
|
|
3293
|
+
<div class="gallery" part="gallery">
|
|
3294
|
+
<div class="gallery-main" part="image">
|
|
2679
3295
|
${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
|
|
2680
3296
|
</div>
|
|
2681
3297
|
${images.length > 1 ? html`
|
|
2682
|
-
<div class="
|
|
2683
|
-
|
|
3298
|
+
<div class="thumbs ${this.thumbsOverflow ? "overflows" : ""}">
|
|
3299
|
+
<button
|
|
3300
|
+
class="thumbs-arrow prev"
|
|
3301
|
+
part="thumbs-arrow"
|
|
3302
|
+
aria-label="Forrige bilder"
|
|
3303
|
+
?disabled=${this.thumbsAtStart}
|
|
3304
|
+
@click=${() => this.scrollThumbs(-1)}
|
|
3305
|
+
>‹</button>
|
|
3306
|
+
<div class="gallery-thumbs" part="thumbs" @scroll=${this.onThumbsScroll}>
|
|
3307
|
+
${images.map(
|
|
2684
3308
|
(img, idx) => html`
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3309
|
+
<button
|
|
3310
|
+
class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
|
|
3311
|
+
part=${idx === this.activeImageIndex ? "thumb thumb-active" : "thumb"}
|
|
3312
|
+
@click=${() => this.activeImageIndex = idx}
|
|
3313
|
+
aria-label=${`Bilde ${idx + 1}`}
|
|
3314
|
+
aria-current=${idx === this.activeImageIndex ? "true" : "false"}
|
|
3315
|
+
>
|
|
3316
|
+
<img src=${img.url} alt="" loading="lazy" />
|
|
3317
|
+
</button>
|
|
3318
|
+
`
|
|
2693
3319
|
)}
|
|
3320
|
+
</div>
|
|
3321
|
+
<button
|
|
3322
|
+
class="thumbs-arrow next"
|
|
3323
|
+
part="thumbs-arrow"
|
|
3324
|
+
aria-label="Neste bilder"
|
|
3325
|
+
?disabled=${this.thumbsAtEnd}
|
|
3326
|
+
@click=${() => this.scrollThumbs(1)}
|
|
3327
|
+
>›</button>
|
|
2694
3328
|
</div>
|
|
2695
3329
|
` : ""}
|
|
2696
3330
|
</div>
|
|
2697
3331
|
|
|
2698
|
-
<div class="info">
|
|
2699
|
-
${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
|
|
2700
|
-
<h2 class="name">${p.title}</h2>
|
|
3332
|
+
<div class="info" part="info">
|
|
3333
|
+
${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
|
|
3334
|
+
<h2 class="name" part="title">${p.title}</h2>
|
|
2701
3335
|
|
|
2702
3336
|
<div class="price-row">
|
|
2703
|
-
<span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
3337
|
+
<span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
2704
3338
|
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
2705
3339
|
compare,
|
|
2706
3340
|
p.price.currency_code
|
|
2707
3341
|
)}</span>` : ""}
|
|
2708
3342
|
</div>
|
|
2709
3343
|
|
|
2710
|
-
<div class="stock ${stock.cls}">${stock.text}</div>
|
|
3344
|
+
<div class="stock ${stock.cls}" part="stock">${stock.text}</div>
|
|
2711
3345
|
|
|
2712
3346
|
${this.renderOptionPickers(p.options)}
|
|
2713
3347
|
|
|
2714
|
-
<div class="
|
|
3348
|
+
<div class="buy" part="buy">
|
|
3349
|
+
${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
|
|
3350
|
+
<vipps-mobilepay-button
|
|
3351
|
+
class="buy-vipps-official"
|
|
3352
|
+
part="buy-vipps"
|
|
3353
|
+
brand="vipps"
|
|
3354
|
+
language="no"
|
|
3355
|
+
verb="buy"
|
|
3356
|
+
variant="primary"
|
|
3357
|
+
rounded="true"
|
|
3358
|
+
stretched="true"
|
|
3359
|
+
role="button"
|
|
3360
|
+
aria-label="Kjøp nå med Vipps"
|
|
3361
|
+
@click=${this.buyWithVipps}
|
|
3362
|
+
></vipps-mobilepay-button>
|
|
3363
|
+
` : html`
|
|
3364
|
+
<button
|
|
3365
|
+
class="buy-vipps"
|
|
3366
|
+
part="buy-vipps"
|
|
3367
|
+
@click=${this.buyWithVipps}
|
|
3368
|
+
?disabled=${this.adding}
|
|
3369
|
+
aria-label="Kjøp nå med Vipps"
|
|
3370
|
+
>
|
|
3371
|
+
<span>Kjøp nå med</span>
|
|
3372
|
+
<span class="vipps-badge">vipps</span>
|
|
3373
|
+
</button>
|
|
3374
|
+
` : ""}
|
|
3375
|
+
|
|
3376
|
+
<div class="qty-cta-row" part="quantity-row">
|
|
2715
3377
|
<div class="qty-stepper">
|
|
2716
3378
|
<button
|
|
2717
3379
|
class="qty-btn"
|
|
@@ -2732,6 +3394,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2732
3394
|
</div>
|
|
2733
3395
|
<button
|
|
2734
3396
|
class="add-to-cart"
|
|
3397
|
+
part="add-to-cart"
|
|
2735
3398
|
@click=${this.addToCart}
|
|
2736
3399
|
?disabled=${this.availableQuantity <= 0 || this.adding}
|
|
2737
3400
|
>
|
|
@@ -2793,36 +3456,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
2793
3456
|
</button>
|
|
2794
3457
|
` : ""}
|
|
2795
3458
|
|
|
2796
|
-
${this.
|
|
2797
|
-
|
|
2798
|
-
class="buy-vipps-official"
|
|
2799
|
-
brand="vipps"
|
|
2800
|
-
language="no"
|
|
2801
|
-
verb="buy"
|
|
2802
|
-
variant="primary"
|
|
2803
|
-
rounded="true"
|
|
2804
|
-
stretched="true"
|
|
2805
|
-
role="button"
|
|
2806
|
-
aria-label="Kjøp nå med Vipps"
|
|
2807
|
-
@click=${this.buyWithVipps}
|
|
2808
|
-
></vipps-mobilepay-button>
|
|
2809
|
-
` : html`
|
|
2810
|
-
<button
|
|
2811
|
-
class="buy-vipps"
|
|
2812
|
-
@click=${this.buyWithVipps}
|
|
2813
|
-
?disabled=${this.adding}
|
|
2814
|
-
aria-label="Kjøp nå med Vipps"
|
|
2815
|
-
>
|
|
2816
|
-
<span>Kjøp nå med</span>
|
|
2817
|
-
<span class="vipps-badge">vipps</span>
|
|
2818
|
-
</button>
|
|
2819
|
-
` : ""}
|
|
3459
|
+
${this.renderSellerNote()}
|
|
3460
|
+
</div>
|
|
2820
3461
|
|
|
2821
3462
|
${p.description ? html`
|
|
2822
|
-
<
|
|
2823
|
-
<
|
|
3463
|
+
<details class="description-block" part="description">
|
|
3464
|
+
<summary>Beskrivelse</summary>
|
|
2824
3465
|
<div class="description">${this.stripHtml(p.description)}</div>
|
|
2825
|
-
</
|
|
3466
|
+
</details>
|
|
2826
3467
|
` : ""}
|
|
2827
3468
|
</div>
|
|
2828
3469
|
</div>
|
|
@@ -2834,9 +3475,9 @@ var VioProductDetail = class extends LitElement {
|
|
|
2834
3475
|
const values = normalizeValues(opt.values);
|
|
2835
3476
|
if (values.length === 0) return "";
|
|
2836
3477
|
return html`
|
|
2837
|
-
<div class="option-group">
|
|
2838
|
-
<div class="option-label">${opt.name}</div>
|
|
2839
|
-
<div class="option-values">
|
|
3478
|
+
<div class="option-group" part="option-group">
|
|
3479
|
+
<div class="option-label" part="option-label">${opt.name}</div>
|
|
3480
|
+
<div class="option-values" part="options">
|
|
2840
3481
|
${values.map((val) => {
|
|
2841
3482
|
const available = this.isValueAvailable(opt.name, val);
|
|
2842
3483
|
const currentVal = this.selectedOptions[opt.name] ?? "";
|
|
@@ -2844,6 +3485,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2844
3485
|
return html`
|
|
2845
3486
|
<button
|
|
2846
3487
|
class="option-pill ${available ? "" : "unavailable"}"
|
|
3488
|
+
part=${isSelected ? "option option-selected" : "option"}
|
|
2847
3489
|
@click=${() => this.selectOption(opt.name, val)}
|
|
2848
3490
|
aria-pressed=${isSelected}
|
|
2849
3491
|
?disabled=${!available}
|
|
@@ -2923,6 +3565,18 @@ __decorateClass([
|
|
|
2923
3565
|
__decorateClass([
|
|
2924
3566
|
state()
|
|
2925
3567
|
], VioProductDetail.prototype, "activeImageIndex", 2);
|
|
3568
|
+
__decorateClass([
|
|
3569
|
+
state()
|
|
3570
|
+
], VioProductDetail.prototype, "thumbsOverflow", 2);
|
|
3571
|
+
__decorateClass([
|
|
3572
|
+
state()
|
|
3573
|
+
], VioProductDetail.prototype, "thumbsAtStart", 2);
|
|
3574
|
+
__decorateClass([
|
|
3575
|
+
state()
|
|
3576
|
+
], VioProductDetail.prototype, "thumbsAtEnd", 2);
|
|
3577
|
+
__decorateClass([
|
|
3578
|
+
state()
|
|
3579
|
+
], VioProductDetail.prototype, "logoSquare", 2);
|
|
2926
3580
|
__decorateClass([
|
|
2927
3581
|
state()
|
|
2928
3582
|
], VioProductDetail.prototype, "adding", 2);
|
|
@@ -2974,6 +3628,14 @@ var VioCart = class extends LitElement {
|
|
|
2974
3628
|
container: () => this.renderRoot.querySelector(".drawer"),
|
|
2975
3629
|
close: () => this.close()
|
|
2976
3630
|
});
|
|
3631
|
+
/** Phones: the page behind is locked, and the sheet closes by pulling the bar or the header down. */
|
|
3632
|
+
this.bottomSheet = new BottomSheet({
|
|
3633
|
+
host: this,
|
|
3634
|
+
sheet: () => this.renderRoot.querySelector(".drawer"),
|
|
3635
|
+
backdrop: () => this.renderRoot.querySelector(".backdrop"),
|
|
3636
|
+
dragZones: ".handle, .header",
|
|
3637
|
+
close: () => this.close()
|
|
3638
|
+
});
|
|
2977
3639
|
this.boundOnCartChange = (e) => {
|
|
2978
3640
|
const detail = e.detail;
|
|
2979
3641
|
this.carts = detail.cartsBySponsor;
|
|
@@ -3004,7 +3666,7 @@ var VioCart = class extends LitElement {
|
|
|
3004
3666
|
top: 0;
|
|
3005
3667
|
right: 0;
|
|
3006
3668
|
bottom: 0;
|
|
3007
|
-
width: min(440px, 100%);
|
|
3669
|
+
width: min(var(--vio-cart-width, 440px), 100%);
|
|
3008
3670
|
background: var(--vio-color-surface, #fff);
|
|
3009
3671
|
transform: translateX(100%);
|
|
3010
3672
|
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
|
@@ -3033,8 +3695,11 @@ var VioCart = class extends LitElement {
|
|
|
3033
3695
|
border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
|
|
3034
3696
|
transform: translateY(100%);
|
|
3035
3697
|
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
|
|
3698
|
+
overscroll-behavior: contain;
|
|
3036
3699
|
}
|
|
3037
3700
|
:host([open]) .drawer { transform: translateY(0); }
|
|
3701
|
+
/* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
|
|
3702
|
+
.handle, .header { touch-action: none; }
|
|
3038
3703
|
.handle {
|
|
3039
3704
|
display: flex;
|
|
3040
3705
|
justify-content: center;
|
|
@@ -3354,6 +4019,7 @@ var VioCart = class extends LitElement {
|
|
|
3354
4019
|
}
|
|
3355
4020
|
disconnectedCallback() {
|
|
3356
4021
|
Vio.cart.removeEventListener("change", this.boundOnCartChange);
|
|
4022
|
+
this.bottomSheet.disconnect();
|
|
3357
4023
|
super.disconnectedCallback();
|
|
3358
4024
|
}
|
|
3359
4025
|
close() {
|
|
@@ -3400,10 +4066,16 @@ var VioCart = class extends LitElement {
|
|
|
3400
4066
|
return isMethodEnabled(this.availableMethods, ...names);
|
|
3401
4067
|
}
|
|
3402
4068
|
updated(changed) {
|
|
3403
|
-
if (changed.has("open"))
|
|
4069
|
+
if (changed.has("open")) {
|
|
4070
|
+
this.dialogFocus.update(this.open);
|
|
4071
|
+
this.bottomSheet.update(this.open);
|
|
4072
|
+
}
|
|
3404
4073
|
if (changed.has("open") && this.open) {
|
|
3405
4074
|
this.confirmedOrder = null;
|
|
3406
4075
|
this.preparedTotal = -1;
|
|
4076
|
+
for (const cart of this.carts.values()) {
|
|
4077
|
+
if (cart.cartId) void Vio.cart.refreshFromServer(cart.sponsorId);
|
|
4078
|
+
}
|
|
3407
4079
|
}
|
|
3408
4080
|
if (this.open && this.itemCount > 0) {
|
|
3409
4081
|
const total = this.totalAcrossSponsors;
|
|
@@ -3519,23 +4191,29 @@ var VioCart = class extends LitElement {
|
|
|
3519
4191
|
if (this.itemCount === 0) return;
|
|
3520
4192
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
3521
4193
|
if (firstSponsorId === void 0) return;
|
|
3522
|
-
|
|
4194
|
+
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: firstSponsorId, source: "cart" });
|
|
4195
|
+
void this.startVippsExpressOrCheckout(firstSponsorId, redirect);
|
|
3523
4196
|
}
|
|
3524
4197
|
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
3525
4198
|
async loadVippsButton() {
|
|
3526
4199
|
this.vippsOfficial = await ensureVippsButton();
|
|
3527
4200
|
}
|
|
3528
|
-
async startVippsExpressOrCheckout(sponsorId) {
|
|
4201
|
+
async startVippsExpressOrCheckout(sponsorId, redirect) {
|
|
3529
4202
|
let fallback = true;
|
|
3530
4203
|
try {
|
|
3531
4204
|
await Vio.cart.waitForCartMutations(sponsorId);
|
|
3532
|
-
if (!await Vio.checkout.getVippsExpressEnabled(sponsorId))
|
|
3533
|
-
|
|
3534
|
-
|
|
4205
|
+
if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
|
|
4206
|
+
redirect.abort("express-off");
|
|
4207
|
+
} else {
|
|
4208
|
+
const res = await Vio.checkout.startVippsExpress(sponsorId);
|
|
4209
|
+
fallback = !res?.payment_url;
|
|
4210
|
+
if (fallback) redirect.abort("not-started");
|
|
4211
|
+
}
|
|
3535
4212
|
} catch (err) {
|
|
3536
4213
|
if (typeof console !== "undefined") {
|
|
3537
4214
|
console.warn("[VioCart] Vipps Express not started:", err);
|
|
3538
4215
|
}
|
|
4216
|
+
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
3539
4217
|
}
|
|
3540
4218
|
if (!fallback) return;
|
|
3541
4219
|
this.dispatchEvent(
|
|
@@ -3826,6 +4504,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3826
4504
|
this.open = false;
|
|
3827
4505
|
this.heading = "Kasse";
|
|
3828
4506
|
this.shippingLabel = "Beregnes ved kasse";
|
|
4507
|
+
this.vippsShipsInApp = false;
|
|
3829
4508
|
this.express = false;
|
|
3830
4509
|
this.checkoutState = null;
|
|
3831
4510
|
this.items = [];
|
|
@@ -3941,6 +4620,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3941
4620
|
close: () => this.close(),
|
|
3942
4621
|
canClose: () => !this.paymentInFlight
|
|
3943
4622
|
});
|
|
4623
|
+
/** Phones: the page behind is locked, and the sheet closes by pulling the bar or the top bar down — not mid-payment. */
|
|
4624
|
+
this.bottomSheet = new BottomSheet({
|
|
4625
|
+
host: this,
|
|
4626
|
+
sheet: () => this.renderRoot.querySelector(".modal"),
|
|
4627
|
+
backdrop: () => this.renderRoot.querySelector(".backdrop"),
|
|
4628
|
+
dragZones: ".handle, .topbar",
|
|
4629
|
+
close: () => this.close(),
|
|
4630
|
+
canClose: () => !this.paymentInFlight
|
|
4631
|
+
});
|
|
3944
4632
|
this.paymentMethodsResolved = false;
|
|
3945
4633
|
/** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
|
|
3946
4634
|
this.autoSelectAttempted = false;
|
|
@@ -4093,8 +4781,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4093
4781
|
from the right instead of the full-screen overlay. */
|
|
4094
4782
|
.modal.as-side {
|
|
4095
4783
|
inset: 0 0 0 auto;
|
|
4096
|
-
width: min(440px, 100%);
|
|
4097
|
-
max-width: 440px;
|
|
4784
|
+
width: min(var(--vio-checkout-width, 440px), 100%);
|
|
4785
|
+
max-width: var(--vio-checkout-width, 440px);
|
|
4098
4786
|
transform: translateX(100%);
|
|
4099
4787
|
border-radius: 0;
|
|
4100
4788
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
@@ -4109,8 +4797,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4109
4797
|
@media (min-width: 601px) {
|
|
4110
4798
|
.modal {
|
|
4111
4799
|
inset: 0 0 0 auto;
|
|
4112
|
-
width: min(440px, 100%);
|
|
4113
|
-
max-width: 440px;
|
|
4800
|
+
width: min(var(--vio-checkout-width, 440px), 100%);
|
|
4801
|
+
max-width: var(--vio-checkout-width, 440px);
|
|
4114
4802
|
transform: translateX(100%);
|
|
4115
4803
|
border-radius: 0;
|
|
4116
4804
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
@@ -4147,7 +4835,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4147
4835
|
height: 92vh;
|
|
4148
4836
|
border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
|
|
4149
4837
|
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
|
|
4838
|
+
overscroll-behavior: contain;
|
|
4150
4839
|
}
|
|
4840
|
+
/* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
|
|
4841
|
+
.handle, .topbar { touch-action: none; }
|
|
4151
4842
|
.handle {
|
|
4152
4843
|
display: flex;
|
|
4153
4844
|
justify-content: center;
|
|
@@ -4522,7 +5213,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4522
5213
|
width: 48px;
|
|
4523
5214
|
height: 48px;
|
|
4524
5215
|
border-radius: 50%;
|
|
4525
|
-
background: #2e7d32;
|
|
5216
|
+
background: var(--vio-confirm-check-bg, #2e7d32);
|
|
4526
5217
|
color: #fff;
|
|
4527
5218
|
display: flex;
|
|
4528
5219
|
align-items: center;
|
|
@@ -4532,8 +5223,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4532
5223
|
}
|
|
4533
5224
|
.confirm-title {
|
|
4534
5225
|
font-family: var(--vio-font-serif, Georgia, serif);
|
|
4535
|
-
font-size: 24px;
|
|
4536
|
-
font-weight: 400;
|
|
5226
|
+
font-size: var(--vio-confirm-title-size, 24px);
|
|
5227
|
+
font-weight: var(--vio-confirm-title-weight, 400);
|
|
4537
5228
|
margin: 0;
|
|
4538
5229
|
}
|
|
4539
5230
|
/* The brand the order is with: its logo over the thanks, its name in it. */
|
|
@@ -4541,13 +5232,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4541
5232
|
.confirm-brand-logo {
|
|
4542
5233
|
display: block;
|
|
4543
5234
|
width: auto;
|
|
4544
|
-
height:
|
|
4545
|
-
max-
|
|
4546
|
-
max-width: 160px;
|
|
5235
|
+
height: var(--vio-confirm-logo-height, 40px);
|
|
5236
|
+
max-width: var(--vio-confirm-logo-max-width, 220px);
|
|
4547
5237
|
object-fit: contain;
|
|
4548
5238
|
}
|
|
4549
5239
|
.confirm-sponsor {
|
|
4550
|
-
font-size: 14px;
|
|
5240
|
+
font-size: var(--vio-confirm-text-size, 14px);
|
|
4551
5241
|
line-height: 1.4;
|
|
4552
5242
|
color: var(--vio-color-text, #0a0a0a);
|
|
4553
5243
|
margin: 4px 0 0;
|
|
@@ -4771,6 +5461,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4771
5461
|
} else if (outcome === "failed") {
|
|
4772
5462
|
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
4773
5463
|
this.open = true;
|
|
5464
|
+
returnCancelled({ method: vioMethod, sponsorId, source: "checkout", checkoutId });
|
|
4774
5465
|
} else if (sawStatus) {
|
|
4775
5466
|
this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
|
|
4776
5467
|
this.open = true;
|
|
@@ -5113,16 +5804,23 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5113
5804
|
this.unmountNexi();
|
|
5114
5805
|
this.unmountAdyen();
|
|
5115
5806
|
for (const settle of [...this.pendingSuccessEmits]) settle();
|
|
5807
|
+
this.bottomSheet.disconnect();
|
|
5116
5808
|
super.disconnectedCallback();
|
|
5117
5809
|
}
|
|
5118
5810
|
updated(changed) {
|
|
5119
|
-
if (changed.has("open"))
|
|
5811
|
+
if (changed.has("open")) {
|
|
5812
|
+
this.dialogFocus.update(this.open);
|
|
5813
|
+
this.bottomSheet.update(this.open);
|
|
5814
|
+
}
|
|
5120
5815
|
if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
|
|
5121
5816
|
void this.loadAvailableShippings();
|
|
5122
5817
|
}
|
|
5123
5818
|
if (changed.has("open") && this.open) {
|
|
5124
5819
|
this.autoSelectSoleMethod();
|
|
5125
5820
|
}
|
|
5821
|
+
if (changed.has("open") && this.open && this.checkoutState?.sponsorId && !this.orderConfirmed) {
|
|
5822
|
+
void Vio.cart.refreshFromServer(this.checkoutState.sponsorId);
|
|
5823
|
+
}
|
|
5126
5824
|
if ((changed.has("klarnaAvailable") && this.klarnaAvailable || changed.has("applePayAvailable") && this.applePayAvailable) && this.paymentMethodsResolved && !this.checkoutState?.paymentMethod) {
|
|
5127
5825
|
this.autoSelectAttempted = false;
|
|
5128
5826
|
this.autoSelectSoleMethod();
|
|
@@ -5249,6 +5947,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5249
5947
|
if (Array.isArray(methods)) {
|
|
5250
5948
|
this.availableMethods = methods.map((m) => m.name);
|
|
5251
5949
|
this.stripeMode = await Vio.checkout.getStripeMode(spId);
|
|
5950
|
+
this.vippsShipsInApp = this.availableMethods.some((m) => String(m).toLowerCase() === "vipps") ? await Vio.checkout.getVippsExpressEnabled(spId) : false;
|
|
5252
5951
|
} else {
|
|
5253
5952
|
this.availableMethods = null;
|
|
5254
5953
|
}
|
|
@@ -5569,10 +6268,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5569
6268
|
}
|
|
5570
6269
|
if (method === "vipps") {
|
|
5571
6270
|
this.vippsLoading = true;
|
|
6271
|
+
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.checkoutState?.sponsorId, source: "checkout" });
|
|
5572
6272
|
void this.startVippsFromCheckout().catch((err) => {
|
|
5573
6273
|
if (typeof console !== "undefined") {
|
|
5574
6274
|
console.warn("[VioCheckout] Vipps payment could not start:", err);
|
|
5575
6275
|
}
|
|
6276
|
+
redirect.fail(errorText(err));
|
|
5576
6277
|
this.paymentError = friendlyPaymentError(errorText(err));
|
|
5577
6278
|
}).finally(() => {
|
|
5578
6279
|
this.vippsLoading = false;
|
|
@@ -5657,12 +6358,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5657
6358
|
* `visibilitychange`) or the element leaves, the event goes out at once
|
|
5658
6359
|
* without the reference. Exactly one event per call. `afterwards` runs
|
|
5659
6360
|
* right after the event, so a path keeps the order it always had (the cart
|
|
5660
|
-
* is cleared after the event).
|
|
6361
|
+
* is cleared after the event). When that answer already has Vio's order
|
|
6362
|
+
* number, the event carries it too (`orderNumber`): it is the purchase's
|
|
6363
|
+
* `order_id`, and analytics need not ask again.
|
|
5661
6364
|
*/
|
|
5662
6365
|
async emitPaymentSuccess(detail, checkoutId, sponsorId, afterwards) {
|
|
5663
6366
|
let done = false;
|
|
5664
6367
|
const sponsorAtStart = this.sponsorOfOrder(sponsorId, checkoutId);
|
|
5665
|
-
const finish = (
|
|
6368
|
+
const finish = (found = {}) => {
|
|
5666
6369
|
if (done) return;
|
|
5667
6370
|
done = true;
|
|
5668
6371
|
const named = this.sponsorOfOrder(sponsorId, checkoutId);
|
|
@@ -5671,7 +6374,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5671
6374
|
const event = new CustomEvent("vio:payment-success", {
|
|
5672
6375
|
bubbles: true,
|
|
5673
6376
|
composed: true,
|
|
5674
|
-
detail: buyerRef ? {
|
|
6377
|
+
detail: found.buyerRef || found.orderNumber ? {
|
|
6378
|
+
...withSponsor,
|
|
6379
|
+
...found.buyerRef ? { buyerRef: found.buyerRef } : {},
|
|
6380
|
+
...found.orderNumber ? { orderNumber: found.orderNumber } : {}
|
|
6381
|
+
} : withSponsor
|
|
5675
6382
|
});
|
|
5676
6383
|
this.dispatchEvent(event);
|
|
5677
6384
|
if (!this.isConnected && typeof window !== "undefined") {
|
|
@@ -5684,7 +6391,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5684
6391
|
};
|
|
5685
6392
|
const known = buyerRefOf(detail);
|
|
5686
6393
|
if (known || !checkoutId || !this.shouldAskBuyerRef(detail)) {
|
|
5687
|
-
finish(known);
|
|
6394
|
+
finish({ buyerRef: known });
|
|
5688
6395
|
return;
|
|
5689
6396
|
}
|
|
5690
6397
|
const settleEarly = () => finish();
|
|
@@ -5716,8 +6423,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5716
6423
|
* failed or has neither the reference nor an order yet (the webhook has not
|
|
5717
6424
|
* written the checkout), again at 1.2 s — all within 2.5 s. An order
|
|
5718
6425
|
* without a reference is a settled answer: the backend has none, asking
|
|
5719
|
-
* again cannot help.
|
|
5720
|
-
*
|
|
6426
|
+
* again cannot help. Whatever the answer said of the order number comes
|
|
6427
|
+
* back with it. `{}` when the time is up, when the answer stopped mattering
|
|
6428
|
+
* (`stopped()`) or on failure.
|
|
5721
6429
|
*/
|
|
5722
6430
|
async askBuyerRef(checkoutId, sponsorId, stopped) {
|
|
5723
6431
|
const started = Date.now();
|
|
@@ -5730,19 +6438,20 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5730
6438
|
for (const at of [0, _VioCheckout.BUYER_REF_RETRY_AT_MS]) {
|
|
5731
6439
|
const wait = Math.min(at - (Date.now() - started), remaining());
|
|
5732
6440
|
if (wait > 0) await Promise.race([new Promise((resolve) => setTimeout(resolve, wait)), timeUp]);
|
|
5733
|
-
if (stopped() || remaining() <= 0) return
|
|
6441
|
+
if (stopped() || remaining() <= 0) return {};
|
|
5734
6442
|
try {
|
|
5735
6443
|
const reference = await Promise.race([
|
|
5736
6444
|
Vio.checkout.getOrderReference(checkoutId, sponsorId || void 0),
|
|
5737
6445
|
timeUp
|
|
5738
6446
|
]);
|
|
5739
|
-
if (reference === null) return
|
|
5740
|
-
|
|
5741
|
-
if (reference.
|
|
6447
|
+
if (reference === null) return {};
|
|
6448
|
+
const order = reference.orderNumber ? { orderNumber: reference.orderNumber } : {};
|
|
6449
|
+
if (reference.buyerRef) return { buyerRef: reference.buyerRef, ...order };
|
|
6450
|
+
if (reference.orderNumber !== null) return order;
|
|
5742
6451
|
} catch {
|
|
5743
6452
|
}
|
|
5744
6453
|
}
|
|
5745
|
-
return
|
|
6454
|
+
return {};
|
|
5746
6455
|
} finally {
|
|
5747
6456
|
clearTimeout(deadline);
|
|
5748
6457
|
}
|
|
@@ -5814,6 +6523,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5814
6523
|
* express always has one; the normal flow has one once the backend
|
|
5815
6524
|
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
5816
6525
|
shippingMajor() {
|
|
6526
|
+
if (this.vippsChoosesShipping()) return 0;
|
|
5817
6527
|
if (this.checkoutState?.paymentMethod === "qliro" && typeof this.qliroShipping?.price === "number") {
|
|
5818
6528
|
return this.qliroShipping.price;
|
|
5819
6529
|
}
|
|
@@ -5828,6 +6538,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5828
6538
|
if (!o) return 0;
|
|
5829
6539
|
return o.priceMajor ?? o.price / 100;
|
|
5830
6540
|
}
|
|
6541
|
+
/** Vipps is the method and it runs as Express: the delivery is chosen in Vipps. */
|
|
6542
|
+
vippsChoosesShipping() {
|
|
6543
|
+
return this.checkoutState?.paymentMethod === "vipps" && this.vippsShipsInApp;
|
|
6544
|
+
}
|
|
5831
6545
|
/** The rate the summary names — Qliro's own pick while Qliro is the method. */
|
|
5832
6546
|
summaryShippingName() {
|
|
5833
6547
|
if (this.checkoutState?.paymentMethod === "qliro" && this.qliroShipping) {
|
|
@@ -6006,22 +6720,22 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6006
6720
|
if (email) details.push(["Kvittering til", email]);
|
|
6007
6721
|
const sponsor = this.checkoutSponsorIdentity();
|
|
6008
6722
|
return html`
|
|
6009
|
-
<section class="confirmation">
|
|
6723
|
+
<section class="confirmation" part="confirmation">
|
|
6010
6724
|
${usableLogo(sponsor) ? html`<div class="confirm-brand" part="sponsor">
|
|
6011
6725
|
${renderSponsorMark(this, sponsor, { logo: "confirm-brand-logo", name: "confirm-brand-name" })}
|
|
6012
6726
|
</div>` : ""}
|
|
6013
6727
|
<header class="confirm-head">
|
|
6014
6728
|
<div class="confirm-check" aria-hidden="true">✓</div>
|
|
6015
6729
|
<div>
|
|
6016
|
-
<h3 class="confirm-title">Takk for bestillingen!</h3>
|
|
6017
|
-
${sponsor?.name ? html`<p class="confirm-sponsor">Du har handlet hos <strong>${sponsor.name}</strong>.</p>` : ""}
|
|
6730
|
+
<h3 class="confirm-title" part="confirm-title">Takk for bestillingen!</h3>
|
|
6731
|
+
${sponsor?.name ? html`<p class="confirm-sponsor" part="confirm-sponsor">Du har handlet hos <strong>${sponsor.name}</strong>.</p>` : ""}
|
|
6018
6732
|
<p class="confirm-text">
|
|
6019
6733
|
Betalingen er bekreftet.${email ? html` Du får en ordrebekreftelse på e-post.` : ""}
|
|
6020
6734
|
</p>
|
|
6021
6735
|
</div>
|
|
6022
6736
|
</header>
|
|
6023
6737
|
|
|
6024
|
-
<dl class="confirm-details">
|
|
6738
|
+
<dl class="confirm-details" part="confirm-details">
|
|
6025
6739
|
${details.map(
|
|
6026
6740
|
([k, v]) => html`<div><dt>${k}</dt><dd class=${k === o?.providerRef?.label ? "ref" : ""}>${v}</dd></div>`
|
|
6027
6741
|
)}
|
|
@@ -6431,12 +7145,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6431
7145
|
for (const delay of _VioCheckout.NEXI_DETAILS_DELAYS_MS) {
|
|
6432
7146
|
if (delay) await new Promise((r) => setTimeout(r, delay));
|
|
6433
7147
|
const o = await Vio.checkout.getNexiOrder(checkoutId, sponsorId).catch(() => null);
|
|
6434
|
-
const
|
|
6435
|
-
if (!
|
|
7148
|
+
const current2 = this.confirmedOrder;
|
|
7149
|
+
if (!current2 || current2.providerRef?.value !== paymentId) return;
|
|
6436
7150
|
if (!o) continue;
|
|
6437
7151
|
const paidWith = o.payment_method ? o.card_last4 ? `${o.payment_method} \u2022\u2022\u2022\u2022 ${o.card_last4}` : o.payment_method : void 0;
|
|
6438
7152
|
this.confirmedOrder = {
|
|
6439
|
-
...
|
|
7153
|
+
...current2,
|
|
6440
7154
|
...paidWith ? { paidWith } : {},
|
|
6441
7155
|
...o.email ? { email: o.email } : {}
|
|
6442
7156
|
};
|
|
@@ -6464,15 +7178,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6464
7178
|
const options = this.nexiShipping?.options ?? [];
|
|
6465
7179
|
if (options.length < 2) return "";
|
|
6466
7180
|
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
6467
|
-
const
|
|
7181
|
+
const current2 = this.nexiShipping?.shipping_id;
|
|
6468
7182
|
return html`
|
|
6469
7183
|
<div class="ship-select nexi-ship" role="radiogroup" aria-label="Frakt">
|
|
6470
7184
|
${options.map(
|
|
6471
7185
|
(o) => html`
|
|
6472
7186
|
<button
|
|
6473
|
-
class="ship-opt ${
|
|
7187
|
+
class="ship-opt ${current2 === o.id ? "active" : ""}"
|
|
6474
7188
|
role="radio"
|
|
6475
|
-
aria-checked=${
|
|
7189
|
+
aria-checked=${current2 === o.id}
|
|
6476
7190
|
?disabled=${this.nexiPicking}
|
|
6477
7191
|
@click=${() => void this.onPickNexiShipping(o.id)}
|
|
6478
7192
|
>
|
|
@@ -7250,7 +7964,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7250
7964
|
</div>
|
|
7251
7965
|
<div class="order-row">
|
|
7252
7966
|
<span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
|
|
7253
|
-
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
7967
|
+
<span>${this.vippsChoosesShipping() ? "Velges i Vipps" : this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
7254
7968
|
</div>
|
|
7255
7969
|
<div class="order-row total">
|
|
7256
7970
|
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
@@ -7271,6 +7985,9 @@ __decorateClass([
|
|
|
7271
7985
|
__decorateClass([
|
|
7272
7986
|
property({ type: String, attribute: "shipping-label" })
|
|
7273
7987
|
], _VioCheckout.prototype, "shippingLabel", 2);
|
|
7988
|
+
__decorateClass([
|
|
7989
|
+
state()
|
|
7990
|
+
], _VioCheckout.prototype, "vippsShipsInApp", 2);
|
|
7274
7991
|
__decorateClass([
|
|
7275
7992
|
property({ type: Boolean, reflect: true })
|
|
7276
7993
|
], _VioCheckout.prototype, "express", 2);
|
|
@@ -7446,5 +8163,5 @@ function registerVioElements() {
|
|
|
7446
8163
|
}
|
|
7447
8164
|
|
|
7448
8165
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
7449
|
-
//# sourceMappingURL=chunk-
|
|
7450
|
-
//# sourceMappingURL=chunk-
|
|
8166
|
+
//# sourceMappingURL=chunk-L6YYYKQK.js.map
|
|
8167
|
+
//# sourceMappingURL=chunk-L6YYYKQK.js.map
|