@vio-live/web-sdk 0.1.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.
@@ -0,0 +1,3284 @@
1
+ import { __decorateClass, Vio, primaryImageUrl, displayPrice, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-5SPY7TH6.js';
2
+ import { css, LitElement, html } from 'lit';
3
+ import { property, state, customElement, query } from 'lit/decorators.js';
4
+
5
+ // src/ui/tokens.ts
6
+ var VioTokens = {
7
+ color: {
8
+ text: {
9
+ primary: "#0a0a0a",
10
+ secondary: "#666666",
11
+ tertiary: "#999999",
12
+ onPrimary: "#ffffff"
13
+ },
14
+ accent: {
15
+ editorial: "#c14a3b"
16
+ // "REDAKSJONENS FAVORITTER" red
17
+ },
18
+ surface: {
19
+ base: "#ffffff",
20
+ muted: "#f2f2f2",
21
+ // product image background
22
+ hover: "#fafafa"
23
+ },
24
+ border: {
25
+ subtle: "#e5e5e5",
26
+ default: "#cccccc"
27
+ }
28
+ },
29
+ font: {
30
+ serif: '"Tinos", "Playfair Display", Georgia, serif',
31
+ sans: '"Inter", -apple-system, "Helvetica Neue", Arial, sans-serif'
32
+ },
33
+ size: {
34
+ xs: "12px",
35
+ sm: "14px",
36
+ md: "16px",
37
+ lg: "20px",
38
+ xl: "24px",
39
+ "2xl": "32px",
40
+ "3xl": "40px"
41
+ },
42
+ weight: {
43
+ regular: 400,
44
+ medium: 500,
45
+ semibold: 600,
46
+ bold: 700
47
+ },
48
+ tracking: {
49
+ label: "0.1em",
50
+ // uppercase labels (BRAND, RETAILER, REDAKSJONENS FAVORITTER)
51
+ title: "0"
52
+ },
53
+ lineHeight: {
54
+ tight: 1.1,
55
+ snug: 1.3,
56
+ normal: 1.5
57
+ },
58
+ space: {
59
+ xs: "4px",
60
+ sm: "8px",
61
+ md: "16px",
62
+ lg: "24px",
63
+ xl: "32px",
64
+ "2xl": "48px",
65
+ "3xl": "64px"
66
+ },
67
+ radius: {
68
+ none: "0",
69
+ sm: "2px",
70
+ md: "4px",
71
+ lg: "8px",
72
+ full: "9999px"
73
+ }
74
+ };
75
+ var VIO_TOKENS_CSS = `
76
+ :root {
77
+ /* color \u2014 text */
78
+ --vio-color-text: ${VioTokens.color.text.primary};
79
+ --vio-color-text-secondary: ${VioTokens.color.text.secondary};
80
+ --vio-color-text-tertiary: ${VioTokens.color.text.tertiary};
81
+ --vio-color-text-on-primary:${VioTokens.color.text.onPrimary};
82
+
83
+ /* color \u2014 accent */
84
+ --vio-color-accent: ${VioTokens.color.accent.editorial};
85
+
86
+ /* color \u2014 surface */
87
+ --vio-color-surface: ${VioTokens.color.surface.base};
88
+ --vio-color-surface-muted: ${VioTokens.color.surface.muted};
89
+ --vio-color-surface-hover: ${VioTokens.color.surface.hover};
90
+
91
+ /* color \u2014 border */
92
+ --vio-color-border: ${VioTokens.color.border.subtle};
93
+ --vio-color-border-default: ${VioTokens.color.border.default};
94
+
95
+ /* font */
96
+ --vio-font-serif: ${VioTokens.font.serif};
97
+ --vio-font-sans: ${VioTokens.font.sans};
98
+
99
+ /* size */
100
+ --vio-size-xs: ${VioTokens.size.xs};
101
+ --vio-size-sm: ${VioTokens.size.sm};
102
+ --vio-size-md: ${VioTokens.size.md};
103
+ --vio-size-lg: ${VioTokens.size.lg};
104
+ --vio-size-xl: ${VioTokens.size.xl};
105
+
106
+ /* tracking (uppercase letterspacing) */
107
+ --vio-tracking-label: ${VioTokens.tracking.label};
108
+
109
+ /* space */
110
+ --vio-space-xs: ${VioTokens.space.xs};
111
+ --vio-space-sm: ${VioTokens.space.sm};
112
+ --vio-space-md: ${VioTokens.space.md};
113
+ --vio-space-lg: ${VioTokens.space.lg};
114
+ --vio-space-xl: ${VioTokens.space.xl};
115
+
116
+ /* radius */
117
+ --vio-radius-sm: ${VioTokens.radius.sm};
118
+ --vio-radius-md: ${VioTokens.radius.md};
119
+ --vio-radius-lg: ${VioTokens.radius.lg};
120
+ }
121
+ `;
122
+ function injectTokens() {
123
+ if (typeof document === "undefined") return;
124
+ if (document.querySelector('style[data-vio="tokens"]') !== null) return;
125
+ const style = document.createElement("style");
126
+ style.setAttribute("data-vio", "tokens");
127
+ style.textContent = VIO_TOKENS_CSS;
128
+ document.head.appendChild(style);
129
+ }
130
+ injectTokens();
131
+ var VioProduct = class extends LitElement {
132
+ constructor() {
133
+ super(...arguments);
134
+ this.brand = "";
135
+ this.name = "";
136
+ this.price = "";
137
+ this.retailer = "";
138
+ this.imageUrl = "";
139
+ this.productId = "";
140
+ this.sponsorId = "";
141
+ this.hasVariants = false;
142
+ this.inCart = false;
143
+ this.cartQty = 0;
144
+ this.justAdded = false;
145
+ this.boundCartChange = () => this.updateInCart();
146
+ }
147
+ connectedCallback() {
148
+ super.connectedCallback();
149
+ this.updateInCart();
150
+ Vio.cart.addEventListener("change", this.boundCartChange);
151
+ }
152
+ disconnectedCallback() {
153
+ Vio.cart.removeEventListener("change", this.boundCartChange);
154
+ super.disconnectedCallback();
155
+ }
156
+ updated(changed) {
157
+ if (changed.has("productId") || changed.has("sponsorId")) this.updateInCart();
158
+ }
159
+ /** Reflect whether (and how many units of) this product sit in the cart. */
160
+ updateInCart() {
161
+ const pid = Number(this.productId);
162
+ if (!Number.isFinite(pid) || pid <= 0) {
163
+ this.inCart = false;
164
+ this.cartQty = 0;
165
+ return;
166
+ }
167
+ const sid = Number(this.sponsorId);
168
+ let qty = 0;
169
+ for (const cart of Vio.cart.getAllCarts().values()) {
170
+ if (Number.isFinite(sid) && sid > 0 && cart.sponsorId !== sid) continue;
171
+ for (const i of cart.items) if (Number(i.productId) === pid) qty += i.quantity;
172
+ }
173
+ this.inCart = qty > 0;
174
+ this.cartQty = qty;
175
+ }
176
+ onClick() {
177
+ this.dispatchEvent(
178
+ new CustomEvent("vio:product-click", {
179
+ bubbles: true,
180
+ composed: true,
181
+ detail: {
182
+ productId: this.productId,
183
+ sponsorId: this.sponsorId,
184
+ brand: this.brand,
185
+ name: this.name,
186
+ price: this.price,
187
+ retailer: this.retailer,
188
+ imageUrl: this.imageUrl
189
+ }
190
+ })
191
+ );
192
+ }
193
+ onKeydown(e) {
194
+ if (e.key === "Enter" || e.key === " ") {
195
+ e.preventDefault();
196
+ this.onClick();
197
+ }
198
+ }
199
+ /** Quick add-to-cart. If the product has selectable variants, open the detail
200
+ * to pick one instead (the host can't choose for the user). Otherwise emit
201
+ * `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
202
+ onAdd(e) {
203
+ e.stopPropagation();
204
+ if (this.hasVariants) {
205
+ this.onClick();
206
+ return;
207
+ }
208
+ this.dispatchEvent(
209
+ new CustomEvent("vio:product-add", {
210
+ bubbles: true,
211
+ composed: true,
212
+ detail: {
213
+ productId: this.productId,
214
+ sponsorId: this.sponsorId,
215
+ brand: this.brand,
216
+ name: this.name,
217
+ price: this.price,
218
+ retailer: this.retailer,
219
+ imageUrl: this.imageUrl
220
+ }
221
+ })
222
+ );
223
+ this.justAdded = true;
224
+ setTimeout(() => {
225
+ this.justAdded = false;
226
+ }, 450);
227
+ }
228
+ render() {
229
+ return html`
230
+ <div
231
+ class="card"
232
+ @click=${this.onClick}
233
+ @keydown=${this.onKeydown}
234
+ role="button"
235
+ tabindex="0"
236
+ aria-label=${`${this.brand} ${this.name} ${this.price}`}
237
+ >
238
+ <div class="image">
239
+ ${this.imageUrl ? html`<img src=${this.imageUrl} alt=${this.name || this.brand} loading="lazy" />` : ""}
240
+ <button
241
+ class="add-btn ${this.inCart ? "in-cart" : ""} ${this.justAdded ? "pop" : ""}"
242
+ @click=${this.onAdd}
243
+ aria-label=${this.inCart ? "I handlekurven \u2013 legg til en til" : "Legg i kurv"}
244
+ title=${this.inCart ? "I handlekurven" : "Legg i kurv"}
245
+ >
246
+ ${this.inCart ? this.cartQty > 1 ? html`<span class="add-count">${this.cartQty}</span>` : html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
247
+ <polyline points="20 6 9 17 4 12"></polyline>
248
+ </svg>` : html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
249
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
250
+ <line x1="3" y1="6" x2="21" y2="6"></line>
251
+ <path d="M16 10a4 4 0 0 1-8 0"></path>
252
+ </svg>`}
253
+ </button>
254
+ </div>
255
+ <div class="meta">
256
+ ${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
257
+ ${this.name ? html`<div class="name">${this.name}</div>` : ""}
258
+ ${this.price ? html`<div class="price">${this.price}</div>` : ""}
259
+ ${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
260
+ </div>
261
+ </div>
262
+ `;
263
+ }
264
+ };
265
+ VioProduct.styles = css`
266
+ :host {
267
+ display: block;
268
+ box-sizing: border-box;
269
+ width: 100%;
270
+ cursor: pointer;
271
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
272
+ color: var(--vio-color-text, #0a0a0a);
273
+ -webkit-tap-highlight-color: transparent;
274
+ }
275
+ .card {
276
+ display: flex;
277
+ flex-direction: column;
278
+ height: 100%;
279
+ }
280
+ .image {
281
+ position: relative;
282
+ aspect-ratio: 1 / 1;
283
+ width: 100%;
284
+ background-color: var(--vio-color-surface-muted, #f2f2f2);
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ overflow: hidden;
289
+ transition: opacity 0.2s ease;
290
+ }
291
+ :host(:hover) .image img { opacity: 0.92; }
292
+
293
+ /* Quick add-to-cart button — overlaid bottom-right of the image, like the
294
+ reference card's wishlist heart but wired to add the product to the cart. */
295
+ .add-btn {
296
+ position: absolute;
297
+ right: 10px;
298
+ bottom: 10px;
299
+ width: 40px;
300
+ height: 40px;
301
+ border-radius: 50%;
302
+ background: rgba(255, 255, 255, 0.96);
303
+ border: none;
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ cursor: pointer;
308
+ color: #0a0a0a;
309
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
310
+ transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
311
+ padding: 0;
312
+ z-index: 2;
313
+ }
314
+ .add-btn:hover { background: #fff; transform: scale(1.08); }
315
+ .add-btn:active { transform: scale(0.95); }
316
+ /* In cart → black circle with a white tick (matches the floating cart). */
317
+ .add-btn.in-cart { background: #0a0a0a; color: #fff; }
318
+ .add-btn.in-cart:hover { background: #222; }
319
+ .add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
320
+ .add-btn svg { width: 18px; height: 18px; }
321
+ .add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
322
+ @keyframes vio-add-pop {
323
+ 0% { transform: scale(1); }
324
+ 35% { transform: scale(1.32); }
325
+ 60% { transform: scale(0.9); }
326
+ 100% { transform: scale(1); }
327
+ }
328
+ .image img {
329
+ width: 100%;
330
+ height: 100%;
331
+ object-fit: cover;
332
+ display: block;
333
+ }
334
+ .meta {
335
+ padding-top: 14px;
336
+ display: flex;
337
+ flex-direction: column;
338
+ flex: 1;
339
+ }
340
+ .brand {
341
+ font-size: var(--vio-size-xs, 12px);
342
+ letter-spacing: var(--vio-tracking-label, 0.1em);
343
+ text-transform: uppercase;
344
+ color: var(--vio-color-text-secondary, #666);
345
+ font-weight: 500;
346
+ margin-bottom: 6px;
347
+ }
348
+ .name {
349
+ font-size: var(--vio-size-md, 16px);
350
+ font-weight: 500;
351
+ line-height: 1.3;
352
+ margin: 0 0 10px;
353
+ display: -webkit-box;
354
+ -webkit-line-clamp: 2;
355
+ line-clamp: 2;
356
+ -webkit-box-orient: vertical;
357
+ overflow: hidden;
358
+ }
359
+ .price {
360
+ font-size: var(--vio-size-md, 16px);
361
+ font-weight: 700;
362
+ margin-bottom: 16px;
363
+ }
364
+ .retailer {
365
+ font-size: var(--vio-size-xs, 12px);
366
+ letter-spacing: var(--vio-tracking-label, 0.1em);
367
+ text-transform: uppercase;
368
+ color: var(--vio-color-text-tertiary, #999);
369
+ font-weight: 500;
370
+ margin-top: auto;
371
+ }
372
+ `;
373
+ __decorateClass([
374
+ property({ type: String })
375
+ ], VioProduct.prototype, "brand", 2);
376
+ __decorateClass([
377
+ property({ type: String })
378
+ ], VioProduct.prototype, "name", 2);
379
+ __decorateClass([
380
+ property({ type: String })
381
+ ], VioProduct.prototype, "price", 2);
382
+ __decorateClass([
383
+ property({ type: String })
384
+ ], VioProduct.prototype, "retailer", 2);
385
+ __decorateClass([
386
+ property({ type: String, attribute: "image-url" })
387
+ ], VioProduct.prototype, "imageUrl", 2);
388
+ __decorateClass([
389
+ property({ type: String, attribute: "product-id" })
390
+ ], VioProduct.prototype, "productId", 2);
391
+ __decorateClass([
392
+ property({ type: String, attribute: "sponsor-id" })
393
+ ], VioProduct.prototype, "sponsorId", 2);
394
+ __decorateClass([
395
+ property({ type: Boolean, attribute: "has-variants" })
396
+ ], VioProduct.prototype, "hasVariants", 2);
397
+ __decorateClass([
398
+ state()
399
+ ], VioProduct.prototype, "inCart", 2);
400
+ __decorateClass([
401
+ state()
402
+ ], VioProduct.prototype, "cartQty", 2);
403
+ __decorateClass([
404
+ state()
405
+ ], VioProduct.prototype, "justAdded", 2);
406
+ VioProduct = __decorateClass([
407
+ customElement("vio-product")
408
+ ], VioProduct);
409
+ var VioProductCarousel = class extends LitElement {
410
+ constructor() {
411
+ super(...arguments);
412
+ this.label = "";
413
+ this.heading = "";
414
+ this.disclaimer = "";
415
+ this.seeAllHref = "";
416
+ this.seeAllLabel = "Se alle";
417
+ this.productIds = "";
418
+ this.commerceKey = "";
419
+ this.sponsorId = 0;
420
+ this.productRefs = "";
421
+ this.currency = "NOK";
422
+ this.fetched = null;
423
+ this.isLoading = false;
424
+ this.fetchError = "";
425
+ }
426
+ connectedCallback() {
427
+ super.connectedCallback();
428
+ if (this.productRefs.trim() || this.productIds.trim()) {
429
+ void this.autoFetch();
430
+ }
431
+ }
432
+ updated(changed) {
433
+ const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
434
+ if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
435
+ void this.autoFetch();
436
+ }
437
+ }
438
+ async autoFetch() {
439
+ if (!Vio.isInitialized) {
440
+ this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
441
+ return;
442
+ }
443
+ this.isLoading = true;
444
+ this.fetchError = "";
445
+ try {
446
+ const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
447
+ if (refs.length === 0) {
448
+ this.fetched = [];
449
+ return;
450
+ }
451
+ const bySponsor = /* @__PURE__ */ new Map();
452
+ for (const r of refs) {
453
+ const arr = bySponsor.get(r.sponsorId) ?? [];
454
+ arr.push(r.productId);
455
+ bySponsor.set(r.sponsorId, arr);
456
+ }
457
+ const fetched = await Promise.all(
458
+ Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
459
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
460
+ const products = await commerce.channel.product.getByIds({
461
+ product_ids: ids,
462
+ currency: this.currency,
463
+ image_size: "large"
464
+ });
465
+ return { sponsorId, products };
466
+ })
467
+ );
468
+ const lookup = /* @__PURE__ */ new Map();
469
+ for (const { sponsorId, products } of fetched) {
470
+ for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
471
+ }
472
+ this.fetched = refs.map(({ sponsorId, productId }) => {
473
+ const product = lookup.get(`${sponsorId}:${productId}`);
474
+ return product ? { product, sponsorId } : null;
475
+ }).filter((x) => x !== null);
476
+ } catch (err) {
477
+ this.fetchError = err instanceof Error ? err.message : String(err);
478
+ } finally {
479
+ this.isLoading = false;
480
+ }
481
+ }
482
+ parseRefs(raw) {
483
+ return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
484
+ const [sidStr, pidStr] = pair.split(":");
485
+ const sid = parseInt((sidStr ?? "").trim(), 10);
486
+ const pid = parseInt((pidStr ?? "").trim(), 10);
487
+ if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
488
+ return { sponsorId: sid, productId: pid };
489
+ }).filter((x) => x !== null);
490
+ }
491
+ scrollByDirection(direction) {
492
+ if (!this.scroller) return;
493
+ const item = this.scroller.querySelector(":scope > *");
494
+ const step = (item?.offsetWidth ?? 240) + 16;
495
+ this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
496
+ }
497
+ renderScrollerContent() {
498
+ if (this.isLoading && !this.fetched) {
499
+ return html`${Array.from({ length: 5 }).map(
500
+ () => html`
501
+ <div class="skeleton">
502
+ <div class="skeleton-image"></div>
503
+ <div class="skeleton-text"></div>
504
+ <div class="skeleton-text shorter"></div>
505
+ </div>
506
+ `
507
+ )}`;
508
+ }
509
+ if (this.fetched && this.fetched.length > 0) {
510
+ return html`${this.fetched.map(({ product: p, sponsorId }) => {
511
+ const img = primaryImageUrl(p) ?? "";
512
+ const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
513
+ return html`
514
+ <vio-product
515
+ product-id=${String(p.id)}
516
+ sponsor-id=${String(sponsorId)}
517
+ brand=${p.brand ?? ""}
518
+ name=${p.title}
519
+ price=${displayPrice(p)}
520
+ retailer=${retailer}
521
+ image-url=${img}
522
+ ?has-variants=${(p.variants?.length ?? 0) > 1}
523
+ ></vio-product>
524
+ `;
525
+ })}`;
526
+ }
527
+ return html`<slot></slot>`;
528
+ }
529
+ render() {
530
+ return html`
531
+ <div class="header">
532
+ <div class="header-text">
533
+ ${this.label ? html`<div class="label">${this.label}</div>` : ""}
534
+ <div class="heading-row">
535
+ ${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
536
+ ${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
537
+ </div>
538
+ </div>
539
+ ${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
540
+ </div>
541
+ <div class="viewport">
542
+ <button
543
+ class="nav-btn prev"
544
+ @click=${() => this.scrollByDirection(-1)}
545
+ aria-label="Forrige"
546
+ >
547
+ ‹
548
+ </button>
549
+ <div class="scroller">${this.renderScrollerContent()}</div>
550
+ <button
551
+ class="nav-btn next"
552
+ @click=${() => this.scrollByDirection(1)}
553
+ aria-label="Neste"
554
+ >
555
+ ›
556
+ </button>
557
+ </div>
558
+ ${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
559
+ `;
560
+ }
561
+ };
562
+ VioProductCarousel.styles = css`
563
+ :host {
564
+ display: block;
565
+ box-sizing: border-box;
566
+ width: 100%;
567
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
568
+ color: var(--vio-color-text, #0a0a0a);
569
+ padding: var(--vio-space-xl, 32px) 0;
570
+ }
571
+ .header {
572
+ display: flex;
573
+ align-items: flex-end;
574
+ justify-content: space-between;
575
+ gap: var(--vio-space-md, 16px);
576
+ margin-bottom: var(--vio-space-lg, 24px);
577
+ padding: 0 var(--vio-space-lg, 24px);
578
+ }
579
+ .header-text { flex: 1; min-width: 0; }
580
+ .label {
581
+ font-size: var(--vio-size-xs, 12px);
582
+ letter-spacing: var(--vio-tracking-label, 0.1em);
583
+ text-transform: uppercase;
584
+ color: var(--vio-color-accent, #c14a3b);
585
+ font-weight: 500;
586
+ margin-bottom: var(--vio-space-sm, 8px);
587
+ }
588
+ .heading-row {
589
+ display: flex;
590
+ align-items: baseline;
591
+ gap: var(--vio-space-sm, 8px);
592
+ flex-wrap: wrap;
593
+ }
594
+ .heading {
595
+ font-family: var(--vio-font-serif, Georgia, serif);
596
+ font-size: clamp(28px, 4vw, 40px);
597
+ font-weight: 400;
598
+ line-height: 1.1;
599
+ margin: 0;
600
+ }
601
+ .disclaimer {
602
+ color: var(--vio-color-text-tertiary, #999);
603
+ font-size: var(--vio-size-sm, 14px);
604
+ font-style: italic;
605
+ }
606
+ .see-all {
607
+ font-size: var(--vio-size-sm, 14px);
608
+ color: var(--vio-color-text, #0a0a0a);
609
+ text-decoration: underline;
610
+ text-underline-offset: 4px;
611
+ white-space: nowrap;
612
+ font-weight: 500;
613
+ }
614
+ .see-all:hover { color: var(--vio-color-text-secondary, #666); }
615
+
616
+ .viewport {
617
+ position: relative;
618
+ }
619
+ .scroller {
620
+ display: flex;
621
+ gap: var(--vio-space-md, 16px);
622
+ /* Extra left padding so the first card clears the prev chevron. */
623
+ padding: 0 var(--vio-space-lg, 24px) 8px 72px;
624
+ overflow-x: auto;
625
+ scroll-snap-type: x mandatory;
626
+ scrollbar-width: none;
627
+ -ms-overflow-style: none;
628
+ -webkit-overflow-scrolling: touch;
629
+ }
630
+ @media (max-width: 600px) {
631
+ .scroller { padding: 0 var(--vio-space-lg, 24px) 8px; }
632
+ }
633
+ .scroller::-webkit-scrollbar { display: none; }
634
+ ::slotted(*),
635
+ vio-product {
636
+ flex: 0 0 240px;
637
+ scroll-snap-align: start;
638
+ }
639
+ @media (max-width: 600px) {
640
+ ::slotted(*),
641
+ vio-product {
642
+ /* Two complete cards per screen (+ a sliver of the next), matching the
643
+ reference: card = (viewport − side padding − gap) / 2. */
644
+ flex: 0 0 calc((100vw - 64px) / 2);
645
+ }
646
+ }
647
+
648
+ .nav-btn {
649
+ position: absolute;
650
+ top: 30%;
651
+ width: 48px;
652
+ height: 48px;
653
+ border-radius: 50%;
654
+ background: #fff;
655
+ border: none;
656
+ box-shadow: 0 2px 14px rgba(0, 0, 0, 0.1);
657
+ cursor: pointer;
658
+ display: flex;
659
+ align-items: center;
660
+ justify-content: center;
661
+ font-size: 16px;
662
+ color: #0a0a0a;
663
+ z-index: 2;
664
+ transition: background 0.15s, transform 0.15s;
665
+ font-family: inherit;
666
+ padding: 0;
667
+ }
668
+ .nav-btn:hover { background: #fafafa; transform: scale(1.05); }
669
+ .nav-btn:active { transform: scale(0.98); }
670
+ .nav-btn.prev { left: 8px; }
671
+ .nav-btn.next { right: 8px; }
672
+ @media (max-width: 600px) {
673
+ .nav-btn { display: none; }
674
+ }
675
+
676
+ .state {
677
+ padding: 60px var(--vio-space-lg, 24px);
678
+ text-align: center;
679
+ color: var(--vio-color-text-secondary, #666);
680
+ font-size: 14px;
681
+ }
682
+ .state.error { color: var(--vio-color-accent, #c14a3b); }
683
+ .skeleton {
684
+ flex: 0 0 240px;
685
+ scroll-snap-align: start;
686
+ }
687
+ .skeleton-image {
688
+ aspect-ratio: 1 / 1;
689
+ background: var(--vio-color-surface-muted, #f2f2f2);
690
+ animation: vio-pulse 1.6s ease-in-out infinite;
691
+ }
692
+ .skeleton-text {
693
+ height: 12px;
694
+ background: var(--vio-color-surface-muted, #f2f2f2);
695
+ margin-top: 12px;
696
+ animation: vio-pulse 1.6s ease-in-out infinite;
697
+ }
698
+ .skeleton-text.shorter { width: 60%; }
699
+ @keyframes vio-pulse {
700
+ 0%,
701
+ 100% { opacity: 1; }
702
+ 50% { opacity: 0.5; }
703
+ }
704
+ `;
705
+ __decorateClass([
706
+ property({ type: String })
707
+ ], VioProductCarousel.prototype, "label", 2);
708
+ __decorateClass([
709
+ property({ type: String })
710
+ ], VioProductCarousel.prototype, "heading", 2);
711
+ __decorateClass([
712
+ property({ type: String })
713
+ ], VioProductCarousel.prototype, "disclaimer", 2);
714
+ __decorateClass([
715
+ property({ type: String, attribute: "see-all-href" })
716
+ ], VioProductCarousel.prototype, "seeAllHref", 2);
717
+ __decorateClass([
718
+ property({ type: String, attribute: "see-all-label" })
719
+ ], VioProductCarousel.prototype, "seeAllLabel", 2);
720
+ __decorateClass([
721
+ property({ type: String, attribute: "product-ids" })
722
+ ], VioProductCarousel.prototype, "productIds", 2);
723
+ __decorateClass([
724
+ property({ type: String, attribute: "commerce-key" })
725
+ ], VioProductCarousel.prototype, "commerceKey", 2);
726
+ __decorateClass([
727
+ property({ type: Number, attribute: "sponsor-id" })
728
+ ], VioProductCarousel.prototype, "sponsorId", 2);
729
+ __decorateClass([
730
+ property({ type: String, attribute: "product-refs" })
731
+ ], VioProductCarousel.prototype, "productRefs", 2);
732
+ __decorateClass([
733
+ property({ type: String })
734
+ ], VioProductCarousel.prototype, "currency", 2);
735
+ __decorateClass([
736
+ query(".scroller")
737
+ ], VioProductCarousel.prototype, "scroller", 2);
738
+ __decorateClass([
739
+ state()
740
+ ], VioProductCarousel.prototype, "fetched", 2);
741
+ __decorateClass([
742
+ state()
743
+ ], VioProductCarousel.prototype, "isLoading", 2);
744
+ __decorateClass([
745
+ state()
746
+ ], VioProductCarousel.prototype, "fetchError", 2);
747
+ VioProductCarousel = __decorateClass([
748
+ customElement("vio-product-carousel")
749
+ ], VioProductCarousel);
750
+ function sponsorNameFromBootstrap(sponsorId) {
751
+ const boot = Vio.bootstrapCache;
752
+ if (!boot) return void 0;
753
+ if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
754
+ return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
755
+ }
756
+ var VioProductDetail = class extends LitElement {
757
+ constructor() {
758
+ super(...arguments);
759
+ this.open = false;
760
+ this.productId = "";
761
+ this.sponsorId = 0;
762
+ this.autoOpen = true;
763
+ this.currency = "NOK";
764
+ this.product = null;
765
+ this.isLoading = false;
766
+ this.fetchError = "";
767
+ this.selectedOptions = {};
768
+ this.quantity = 1;
769
+ this.activeImageIndex = 0;
770
+ this.adding = false;
771
+ this.applePayOk = false;
772
+ /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
773
+ this.applePayHandle = null;
774
+ this.boundProductClick = (e) => {
775
+ if (!this.autoOpen) return;
776
+ const ev = e;
777
+ const productIdNum = parseInt(ev.detail.productId, 10);
778
+ if (Number.isNaN(productIdNum)) return;
779
+ const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
780
+ if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
781
+ this.productId = ev.detail.productId;
782
+ this.sponsorId = sponsorIdNum;
783
+ this.show();
784
+ void this.fetchProduct();
785
+ };
786
+ }
787
+ connectedCallback() {
788
+ super.connectedCallback();
789
+ document.addEventListener("vio:product-click", this.boundProductClick);
790
+ }
791
+ disconnectedCallback() {
792
+ document.removeEventListener("vio:product-click", this.boundProductClick);
793
+ super.disconnectedCallback();
794
+ }
795
+ show() {
796
+ this.open = true;
797
+ }
798
+ close() {
799
+ this.open = false;
800
+ setTimeout(() => {
801
+ if (!this.open) {
802
+ this.product = null;
803
+ this.selectedOptions = {};
804
+ this.quantity = 1;
805
+ this.activeImageIndex = 0;
806
+ this.fetchError = "";
807
+ }
808
+ }, 400);
809
+ }
810
+ async fetchProduct() {
811
+ const productIdNum = parseInt(this.productId, 10);
812
+ if (Number.isNaN(productIdNum) || !this.sponsorId) {
813
+ this.product = null;
814
+ return;
815
+ }
816
+ this.isLoading = true;
817
+ this.fetchError = "";
818
+ try {
819
+ const commerce = Vio.commerceFor(this.sponsorId);
820
+ const products = await commerce.channel.product.getByIds({
821
+ product_ids: [productIdNum],
822
+ currency: this.currency,
823
+ image_size: "large"
824
+ });
825
+ this.product = products[0] ?? null;
826
+ if (this.product) {
827
+ const initial = {};
828
+ for (const opt of this.product.options) {
829
+ const values = normalizeValues(opt.values);
830
+ if (values.length > 0) initial[opt.name] = values[0];
831
+ }
832
+ this.selectedOptions = initial;
833
+ void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
834
+ this.applePayHandle = handle;
835
+ this.applePayOk = handle.available;
836
+ }).catch(() => {
837
+ this.applePayHandle = null;
838
+ this.applePayOk = false;
839
+ });
840
+ }
841
+ } catch (err) {
842
+ this.fetchError = err instanceof Error ? err.message : String(err);
843
+ } finally {
844
+ this.isLoading = false;
845
+ }
846
+ }
847
+ /** Resolve the active variant from the selected option values. */
848
+ selectedVariant() {
849
+ if (!this.product || this.product.variants.length === 0) return null;
850
+ const vals = Object.values(this.selectedOptions);
851
+ if (vals.length === 0) return this.product.variants[0] ?? null;
852
+ const found = this.product.variants.find((v) => {
853
+ if (!v.title) return false;
854
+ return vals.every(
855
+ (val) => v.title.toLowerCase().includes(val.toLowerCase())
856
+ );
857
+ });
858
+ return found ?? this.product.variants[0] ?? null;
859
+ }
860
+ get unitPrice() {
861
+ const v = this.selectedVariant();
862
+ const price = v?.price ?? this.product?.price;
863
+ return price?.amount_incl_taxes ?? price?.amount ?? 0;
864
+ }
865
+ get compareAt() {
866
+ const v = this.selectedVariant();
867
+ const price = v?.price ?? this.product?.price;
868
+ return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
869
+ }
870
+ get availableQuantity() {
871
+ const v = this.selectedVariant();
872
+ if (v) return v.quantity ?? 0;
873
+ return this.product?.quantity ?? 0;
874
+ }
875
+ stockLabel() {
876
+ const qty = this.availableQuantity;
877
+ if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
878
+ if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
879
+ return { text: "P\xE5 lager", cls: "in-stock" };
880
+ }
881
+ selectOption(optionName, value) {
882
+ this.selectedOptions = { ...this.selectedOptions, [optionName]: value };
883
+ }
884
+ allImages() {
885
+ if (!this.product) return [];
886
+ const variantImages = this.selectedVariant()?.images ?? [];
887
+ if (variantImages.length > 0) return variantImages;
888
+ return this.product.images;
889
+ }
890
+ get activeImage() {
891
+ return this.allImages()[this.activeImageIndex];
892
+ }
893
+ /** Add the current product + selected variant + qty to the cart. Returns the variant id used. */
894
+ addCurrentToCart() {
895
+ if (!this.product) return void 0;
896
+ const variant = this.selectedVariant();
897
+ const sponsor = Vio.bootstrapCache?.primarySponsor;
898
+ const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
899
+ const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
900
+ Vio.cart.addProduct({
901
+ product: this.product,
902
+ sponsorId: this.sponsorId,
903
+ sponsorName,
904
+ quantity: this.quantity,
905
+ variantId: variant?.id
906
+ });
907
+ return variant?.id;
908
+ }
909
+ async addToCart() {
910
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
911
+ this.adding = true;
912
+ try {
913
+ const variantId = this.addCurrentToCart();
914
+ this.dispatchEvent(
915
+ new CustomEvent("vio:added-to-cart", {
916
+ bubbles: true,
917
+ composed: true,
918
+ detail: {
919
+ productId: this.product.id,
920
+ sponsorId: this.sponsorId,
921
+ quantity: this.quantity,
922
+ variantId
923
+ }
924
+ })
925
+ );
926
+ this.close();
927
+ } finally {
928
+ this.adding = false;
929
+ }
930
+ }
931
+ /**
932
+ * Express buy: add to cart, then open the checkout overlay with Klarna
933
+ * preselected. The host app handles `vio:checkout-open` (same event the cart
934
+ * uses) — the `paymentMethod` hint tells it to preselect Klarna so its widget
935
+ * loads immediately. Reuses the working Klarna Payments flow (no express SDK).
936
+ */
937
+ async buyWithKlarna() {
938
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
939
+ this.adding = true;
940
+ try {
941
+ this.addCurrentToCart();
942
+ this.close();
943
+ this.dispatchEvent(
944
+ new CustomEvent("vio:checkout-open", {
945
+ bubbles: true,
946
+ composed: true,
947
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
948
+ })
949
+ );
950
+ } finally {
951
+ this.adding = false;
952
+ }
953
+ }
954
+ /**
955
+ * Express buy with Apple Pay. Runs synchronously inside the click so the
956
+ * Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
957
+ * state) → launch the sheet. On authorize, `payment-complete` drives the
958
+ * confirmation drawer (same path as the in-checkout Apple Pay button).
959
+ */
960
+ buyWithApplePay() {
961
+ if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
962
+ this.addCurrentToCart();
963
+ try {
964
+ Vio.checkout.open(this.sponsorId);
965
+ } catch (err) {
966
+ if (typeof console !== "undefined") {
967
+ console.warn("[VioProductDetail] checkout open failed:", err);
968
+ }
969
+ return;
970
+ }
971
+ void this.applePayHandle.show();
972
+ this.close();
973
+ }
974
+ render() {
975
+ return html`
976
+ <div class="backdrop" @click=${this.close}></div>
977
+ <div class="modal" role="dialog" aria-label="Produkt">
978
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
979
+ <div class="topbar">
980
+ <span class="topbar-brand">${this.product?.brand ?? ""}</span>
981
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
982
+ </div>
983
+
984
+ ${this.renderBody()}
985
+ </div>
986
+ `;
987
+ }
988
+ renderBody() {
989
+ if (this.isLoading && !this.product) {
990
+ return html`
991
+ <div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
992
+ <div class="sk-image"></div>
993
+ <div class="sk-line sk-brand"></div>
994
+ <div class="sk-line sk-title"></div>
995
+ <div class="sk-line sk-title-2"></div>
996
+ <div class="sk-line sk-price"></div>
997
+ <div class="sk-button"></div>
998
+ </div>
999
+ `;
1000
+ }
1001
+ if (this.fetchError) {
1002
+ return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
1003
+ }
1004
+ if (!this.product) return html``;
1005
+ const p = this.product;
1006
+ const stock = this.stockLabel();
1007
+ const compare = this.compareAt;
1008
+ const showCompare = compare !== null && compare > this.unitPrice;
1009
+ const images = this.allImages();
1010
+ const activeImage = this.activeImage;
1011
+ return html`
1012
+ <div class="body">
1013
+ <div class="gallery">
1014
+ <div class="gallery-main">
1015
+ ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
1016
+ </div>
1017
+ ${images.length > 1 ? html`
1018
+ <div class="gallery-thumbs">
1019
+ ${images.map(
1020
+ (img, idx) => html`
1021
+ <button
1022
+ class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
1023
+ @click=${() => this.activeImageIndex = idx}
1024
+ aria-label=${`Bilde ${idx + 1}`}
1025
+ >
1026
+ <img src=${img.url} alt="" />
1027
+ </button>
1028
+ `
1029
+ )}
1030
+ </div>
1031
+ ` : ""}
1032
+ </div>
1033
+
1034
+ <div class="info">
1035
+ ${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
1036
+ <h2 class="name">${p.title}</h2>
1037
+
1038
+ <div class="price-row">
1039
+ <span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
1040
+ ${showCompare ? html`<span class="compare-at">${formatPrice(
1041
+ compare,
1042
+ p.price.currency_code
1043
+ )}</span>` : ""}
1044
+ </div>
1045
+
1046
+ <div class="stock ${stock.cls}">${stock.text}</div>
1047
+
1048
+ ${this.renderOptionPickers(p.options)}
1049
+
1050
+ <div class="qty-cta-row">
1051
+ <div class="qty-stepper">
1052
+ <button
1053
+ class="qty-btn"
1054
+ @click=${() => this.quantity = Math.max(1, this.quantity - 1)}
1055
+ aria-label="Reduser antall"
1056
+ >
1057
+ −
1058
+ </button>
1059
+ <span class="qty-value">${this.quantity}</span>
1060
+ <button
1061
+ class="qty-btn"
1062
+ @click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
1063
+ aria-label="Øk antall"
1064
+ ?disabled=${this.quantity >= this.availableQuantity}
1065
+ >
1066
+ +
1067
+ </button>
1068
+ </div>
1069
+ <button
1070
+ class="add-to-cart"
1071
+ @click=${this.addToCart}
1072
+ ?disabled=${this.availableQuantity <= 0 || this.adding}
1073
+ >
1074
+ ${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
1075
+ </button>
1076
+ </div>
1077
+
1078
+ ${this.availableQuantity > 0 && this.applePayOk ? html`
1079
+ <button
1080
+ class="buy-applepay"
1081
+ @click=${this.buyWithApplePay}
1082
+ ?disabled=${this.adding}
1083
+ aria-label="Kjøp nå med Apple Pay"
1084
+ >
1085
+ Kjøp nå med Pay
1086
+ </button>
1087
+ ` : ""}
1088
+ ${this.availableQuantity > 0 ? html`
1089
+ <button
1090
+ class="buy-klarna"
1091
+ @click=${this.buyWithKlarna}
1092
+ ?disabled=${this.adding}
1093
+ >
1094
+ <span>Kjøp nå med</span>
1095
+ <img
1096
+ class="klarna-badge"
1097
+ src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1098
+ alt="Klarna"
1099
+ />
1100
+ </button>
1101
+ ` : ""}
1102
+
1103
+ ${p.description ? html`
1104
+ <div>
1105
+ <div class="description-label">Beskrivelse</div>
1106
+ <div class="description">${this.stripHtml(p.description)}</div>
1107
+ </div>
1108
+ ` : ""}
1109
+ </div>
1110
+ </div>
1111
+ `;
1112
+ }
1113
+ renderOptionPickers(options) {
1114
+ if (options.length === 0) return "";
1115
+ return html`${options.map((opt) => {
1116
+ const values = normalizeValues(opt.values);
1117
+ if (values.length === 0) return "";
1118
+ return html`
1119
+ <div class="option-group">
1120
+ <div class="option-label">${opt.name}</div>
1121
+ <div class="option-values">
1122
+ ${values.map(
1123
+ (val) => html`
1124
+ <button
1125
+ class="option-pill"
1126
+ @click=${() => this.selectOption(opt.name, val)}
1127
+ aria-pressed=${this.selectedOptions[opt.name] === val}
1128
+ >
1129
+ ${val}
1130
+ </button>
1131
+ `
1132
+ )}
1133
+ </div>
1134
+ </div>
1135
+ `;
1136
+ })}`;
1137
+ }
1138
+ /** Best-effort HTML strip for description. */
1139
+ stripHtml(html6) {
1140
+ return html6.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();
1141
+ }
1142
+ };
1143
+ VioProductDetail.styles = css`
1144
+ :host { display: contents; }
1145
+
1146
+ .backdrop {
1147
+ position: fixed;
1148
+ inset: 0;
1149
+ background: rgba(0, 0, 0, 0.5);
1150
+ opacity: 0;
1151
+ pointer-events: none;
1152
+ transition: opacity 0.25s ease;
1153
+ z-index: 1100;
1154
+ }
1155
+ :host([open]) .backdrop { opacity: 1; pointer-events: auto; }
1156
+
1157
+ .modal {
1158
+ position: fixed;
1159
+ inset: 0;
1160
+ background: var(--vio-color-surface, #fff);
1161
+ transform: translateY(100%);
1162
+ transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
1163
+ display: flex;
1164
+ flex-direction: column;
1165
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
1166
+ color: var(--vio-color-text, #0a0a0a);
1167
+ z-index: 1101;
1168
+ overflow-y: auto;
1169
+ }
1170
+ :host([open]) .modal { transform: translateY(0); }
1171
+
1172
+ .handle { display: none; }
1173
+
1174
+ /* Mobile: bottom sheet (not fullscreen), rounded top, drag handle. */
1175
+ @media (max-width: 600px) {
1176
+ .modal {
1177
+ top: auto;
1178
+ height: 92vh;
1179
+ border-radius: 16px 16px 0 0;
1180
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
1181
+ }
1182
+ .handle {
1183
+ display: flex;
1184
+ justify-content: center;
1185
+ padding: 10px 0 4px;
1186
+ flex-shrink: 0;
1187
+ }
1188
+ .handle::before {
1189
+ content: '';
1190
+ width: 40px;
1191
+ height: 4px;
1192
+ background: var(--vio-color-border, #e5e5e5);
1193
+ border-radius: 2px;
1194
+ }
1195
+ }
1196
+
1197
+ /* Desktop: right-side drawer (matches the cart/checkout), so the product
1198
+ shows "al lado" instead of a centered modal. Info stacks in one column. */
1199
+ @media (min-width: 601px) {
1200
+ .modal {
1201
+ inset: 0 0 0 auto;
1202
+ width: min(440px, 100%);
1203
+ max-width: 440px;
1204
+ border-radius: 0;
1205
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
1206
+ transform: translateX(100%);
1207
+ transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
1208
+ }
1209
+ :host([open]) .modal { transform: translateX(0); }
1210
+ /* Content overrides are prefixed with .modal so they beat the base
1211
+ .body / .name / .gallery-main rules defined LATER in this stylesheet
1212
+ (equal specificity, later wins; the extra class tips the cascade). */
1213
+ .modal .body {
1214
+ max-width: none;
1215
+ grid-template-columns: 1fr;
1216
+ gap: 20px;
1217
+ padding: 20px 24px 40px;
1218
+ }
1219
+ .modal .name { font-size: 26px; line-height: 1.2; }
1220
+ .modal .gallery-main { width: 100%; }
1221
+ }
1222
+
1223
+ .topbar {
1224
+ display: flex;
1225
+ justify-content: space-between;
1226
+ align-items: center;
1227
+ padding: 20px var(--vio-space-lg, 24px);
1228
+ position: sticky;
1229
+ top: 0;
1230
+ background: var(--vio-color-surface, #fff);
1231
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
1232
+ z-index: 1;
1233
+ }
1234
+ .topbar-brand {
1235
+ font-size: 12px;
1236
+ text-transform: uppercase;
1237
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1238
+ color: var(--vio-color-text-secondary, #666);
1239
+ font-weight: 500;
1240
+ }
1241
+ .close {
1242
+ background: none;
1243
+ border: none;
1244
+ cursor: pointer;
1245
+ font-size: 28px;
1246
+ line-height: 1;
1247
+ padding: 0;
1248
+ color: var(--vio-color-text, #0a0a0a);
1249
+ font-family: inherit;
1250
+ }
1251
+ .close:hover { color: var(--vio-color-text-secondary, #666); }
1252
+
1253
+ .body {
1254
+ flex: 1;
1255
+ max-width: 1200px;
1256
+ width: 100%;
1257
+ margin: 0 auto;
1258
+ padding: 48px var(--vio-space-lg, 24px) 80px;
1259
+ box-sizing: border-box;
1260
+ display: grid;
1261
+ grid-template-columns: 1fr 1fr;
1262
+ gap: 64px;
1263
+ }
1264
+ @media (max-width: 800px) {
1265
+ .body { grid-template-columns: 1fr; gap: 32px; padding: 24px var(--vio-space-lg, 24px) 64px; }
1266
+ }
1267
+
1268
+ /* Gallery */
1269
+ .gallery { display: flex; flex-direction: column; gap: 16px; }
1270
+ .gallery-main {
1271
+ aspect-ratio: 1;
1272
+ background: var(--vio-color-surface-muted, #f2f2f2);
1273
+ display: flex;
1274
+ align-items: center;
1275
+ justify-content: center;
1276
+ overflow: hidden;
1277
+ }
1278
+ .gallery-main img {
1279
+ width: 100%;
1280
+ height: 100%;
1281
+ object-fit: contain;
1282
+ padding: 24px;
1283
+ box-sizing: border-box;
1284
+ }
1285
+ .gallery-thumbs {
1286
+ display: flex;
1287
+ gap: 8px;
1288
+ flex-wrap: wrap;
1289
+ }
1290
+ .gallery-thumb {
1291
+ width: 72px;
1292
+ height: 72px;
1293
+ background: var(--vio-color-surface-muted, #f2f2f2);
1294
+ border: 1px solid transparent;
1295
+ cursor: pointer;
1296
+ padding: 0;
1297
+ overflow: hidden;
1298
+ }
1299
+ .gallery-thumb img {
1300
+ width: 100%;
1301
+ height: 100%;
1302
+ object-fit: contain;
1303
+ padding: 6px;
1304
+ box-sizing: border-box;
1305
+ }
1306
+ .gallery-thumb.active { border-color: var(--vio-color-text, #0a0a0a); }
1307
+
1308
+ /* Info column */
1309
+ .info { display: flex; flex-direction: column; gap: 24px; }
1310
+ .brand {
1311
+ font-size: 12px;
1312
+ text-transform: uppercase;
1313
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1314
+ color: var(--vio-color-text-secondary, #666);
1315
+ font-weight: 500;
1316
+ }
1317
+ .name {
1318
+ font-family: var(--vio-font-serif, Georgia, serif);
1319
+ font-size: clamp(28px, 4vw, 40px);
1320
+ font-weight: 400;
1321
+ line-height: 1.15;
1322
+ margin: 0;
1323
+ letter-spacing: -0.01em;
1324
+ }
1325
+ .price-row {
1326
+ display: flex;
1327
+ align-items: baseline;
1328
+ gap: 16px;
1329
+ }
1330
+ .price {
1331
+ font-size: 24px;
1332
+ font-weight: 700;
1333
+ }
1334
+ .compare-at {
1335
+ font-size: 16px;
1336
+ color: var(--vio-color-text-tertiary, #999);
1337
+ text-decoration: line-through;
1338
+ }
1339
+ .stock {
1340
+ font-size: 12px;
1341
+ text-transform: uppercase;
1342
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1343
+ font-weight: 500;
1344
+ }
1345
+ .stock.in-stock { color: #2e7d32; }
1346
+ .stock.low-stock { color: #ed6c02; }
1347
+ .stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
1348
+
1349
+ /* Option pickers */
1350
+ .option-group { display: flex; flex-direction: column; gap: 8px; }
1351
+ .option-label {
1352
+ font-size: 11px;
1353
+ text-transform: uppercase;
1354
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1355
+ color: var(--vio-color-text-secondary, #666);
1356
+ font-weight: 500;
1357
+ }
1358
+ .option-values { display: flex; flex-wrap: wrap; gap: 8px; }
1359
+ .option-pill {
1360
+ padding: 10px 18px;
1361
+ background: #fff;
1362
+ border: 1px solid var(--vio-color-border, #e5e5e5);
1363
+ cursor: pointer;
1364
+ font-family: inherit;
1365
+ font-size: 13px;
1366
+ font-weight: 500;
1367
+ color: inherit;
1368
+ transition: border-color 0.15s, background 0.15s;
1369
+ }
1370
+ .option-pill:hover { border-color: var(--vio-color-text, #0a0a0a); }
1371
+ .option-pill[aria-pressed='true'] {
1372
+ border-color: var(--vio-color-text, #0a0a0a);
1373
+ background: var(--vio-color-text, #0a0a0a);
1374
+ color: var(--vio-color-text-on-primary, #fff);
1375
+ }
1376
+
1377
+ /* Qty + CTA row */
1378
+ .qty-cta-row {
1379
+ display: grid;
1380
+ grid-template-columns: auto 1fr;
1381
+ gap: 16px;
1382
+ align-items: stretch;
1383
+ }
1384
+ .qty-stepper {
1385
+ display: flex;
1386
+ align-items: center;
1387
+ border: 1px solid var(--vio-color-border, #e5e5e5);
1388
+ }
1389
+ .qty-btn {
1390
+ width: 44px;
1391
+ height: 56px;
1392
+ background: #fff;
1393
+ border: none;
1394
+ cursor: pointer;
1395
+ font-size: 18px;
1396
+ color: var(--vio-color-text, #0a0a0a);
1397
+ font-family: inherit;
1398
+ }
1399
+ .qty-btn:hover { background: var(--vio-color-surface-hover, #fafafa); }
1400
+ .qty-value {
1401
+ width: 40px;
1402
+ text-align: center;
1403
+ font-weight: 600;
1404
+ }
1405
+ .add-to-cart {
1406
+ background: var(--vio-color-text, #0a0a0a);
1407
+ color: var(--vio-color-text-on-primary, #fff);
1408
+ border: none;
1409
+ padding: 18px 24px;
1410
+ font-size: 12px;
1411
+ text-transform: uppercase;
1412
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1413
+ font-weight: 600;
1414
+ cursor: pointer;
1415
+ font-family: inherit;
1416
+ transition: background 0.15s;
1417
+ }
1418
+ .add-to-cart:hover:not(:disabled) { background: #222; }
1419
+ .add-to-cart:disabled {
1420
+ background: var(--vio-color-text-tertiary, #999);
1421
+ cursor: not-allowed;
1422
+ }
1423
+ /* Klarna express button — sits full-width below the add-to-cart row. */
1424
+ .buy-klarna {
1425
+ width: 100%;
1426
+ margin-top: 10px;
1427
+ display: flex;
1428
+ align-items: center;
1429
+ justify-content: center;
1430
+ gap: 8px;
1431
+ background: #ffb3c7; /* Klarna pink */
1432
+ color: #0b051d;
1433
+ border: none;
1434
+ padding: 14px 24px;
1435
+ font-size: 12px;
1436
+ text-transform: uppercase;
1437
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1438
+ font-weight: 700;
1439
+ cursor: pointer;
1440
+ font-family: inherit;
1441
+ transition: background 0.15s;
1442
+ }
1443
+ .buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
1444
+ .buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
1445
+ .buy-klarna:disabled { opacity: 0.5; cursor: not-allowed; }
1446
+ /* Apple Pay express button — black, per Apple's button guidelines. Only
1447
+ shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
1448
+ .buy-applepay {
1449
+ width: 100%;
1450
+ margin-top: 10px;
1451
+ display: flex;
1452
+ align-items: center;
1453
+ justify-content: center;
1454
+ gap: 4px;
1455
+ background: #000;
1456
+ color: #fff;
1457
+ border: none;
1458
+ padding: 15px 24px;
1459
+ font-size: 16px;
1460
+ font-weight: 600;
1461
+ cursor: pointer;
1462
+ font-family: inherit;
1463
+ letter-spacing: 0.01em;
1464
+ }
1465
+ .buy-applepay:hover:not(:disabled) { background: #1a1a1a; }
1466
+ .buy-applepay:disabled { opacity: 0.5; cursor: not-allowed; }
1467
+
1468
+ /* Description */
1469
+ .description {
1470
+ font-size: 14px;
1471
+ line-height: 1.6;
1472
+ color: var(--vio-color-text-secondary, #444);
1473
+ margin-top: 8px;
1474
+ }
1475
+ .description-label {
1476
+ font-size: 11px;
1477
+ text-transform: uppercase;
1478
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1479
+ color: var(--vio-color-text-secondary, #666);
1480
+ font-weight: 500;
1481
+ margin-bottom: 8px;
1482
+ }
1483
+
1484
+ /* Skeleton loader: shimmer placeholders mirroring the product layout
1485
+ (image, brand, title, price, CTA) for a smooth load-in. */
1486
+ .skeleton {
1487
+ display: flex;
1488
+ flex-direction: column;
1489
+ gap: 14px;
1490
+ padding: 20px 24px 40px;
1491
+ }
1492
+ .skeleton > * {
1493
+ background: linear-gradient(
1494
+ 100deg,
1495
+ var(--vio-color-surface-muted, #f1f1f1) 30%,
1496
+ rgba(255, 255, 255, 0.65) 50%,
1497
+ var(--vio-color-surface-muted, #f1f1f1) 70%
1498
+ );
1499
+ background-size: 200% 100%;
1500
+ animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
1501
+ border-radius: 8px;
1502
+ }
1503
+ .sk-image { width: 100%; aspect-ratio: 1; border-radius: 12px; }
1504
+ .sk-line { height: 14px; }
1505
+ .sk-brand { width: 38%; height: 10px; margin-top: 6px; }
1506
+ .sk-title { width: 85%; height: 26px; }
1507
+ .sk-title-2 { width: 55%; height: 26px; }
1508
+ .sk-price { width: 30%; height: 20px; margin-top: 6px; }
1509
+ .sk-button { width: 100%; height: 54px; border-radius: 0; margin-top: 10px; }
1510
+ @keyframes vio-skeleton-shimmer {
1511
+ 0% { background-position: 200% 0; }
1512
+ 100% { background-position: -200% 0; }
1513
+ }
1514
+ @media (prefers-reduced-motion: reduce) {
1515
+ .skeleton > * {
1516
+ animation: none;
1517
+ background: var(--vio-color-surface-muted, #f1f1f1);
1518
+ }
1519
+ }
1520
+ .error {
1521
+ padding: 80px 24px;
1522
+ text-align: center;
1523
+ color: var(--vio-color-accent, #c14a3b);
1524
+ font-size: 14px;
1525
+ }
1526
+ `;
1527
+ __decorateClass([
1528
+ property({ type: Boolean, reflect: true })
1529
+ ], VioProductDetail.prototype, "open", 2);
1530
+ __decorateClass([
1531
+ property({ type: String, attribute: "product-id" })
1532
+ ], VioProductDetail.prototype, "productId", 2);
1533
+ __decorateClass([
1534
+ property({ type: Number, attribute: "sponsor-id" })
1535
+ ], VioProductDetail.prototype, "sponsorId", 2);
1536
+ __decorateClass([
1537
+ property({ type: Boolean, attribute: "auto-open" })
1538
+ ], VioProductDetail.prototype, "autoOpen", 2);
1539
+ __decorateClass([
1540
+ property({ type: String })
1541
+ ], VioProductDetail.prototype, "currency", 2);
1542
+ __decorateClass([
1543
+ state()
1544
+ ], VioProductDetail.prototype, "product", 2);
1545
+ __decorateClass([
1546
+ state()
1547
+ ], VioProductDetail.prototype, "isLoading", 2);
1548
+ __decorateClass([
1549
+ state()
1550
+ ], VioProductDetail.prototype, "fetchError", 2);
1551
+ __decorateClass([
1552
+ state()
1553
+ ], VioProductDetail.prototype, "selectedOptions", 2);
1554
+ __decorateClass([
1555
+ state()
1556
+ ], VioProductDetail.prototype, "quantity", 2);
1557
+ __decorateClass([
1558
+ state()
1559
+ ], VioProductDetail.prototype, "activeImageIndex", 2);
1560
+ __decorateClass([
1561
+ state()
1562
+ ], VioProductDetail.prototype, "adding", 2);
1563
+ __decorateClass([
1564
+ state()
1565
+ ], VioProductDetail.prototype, "applePayOk", 2);
1566
+ VioProductDetail = __decorateClass([
1567
+ customElement("vio-product-detail")
1568
+ ], VioProductDetail);
1569
+ function normalizeValues(raw) {
1570
+ if (Array.isArray(raw)) {
1571
+ return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
1572
+ }
1573
+ if (typeof raw === "string") {
1574
+ return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
1575
+ }
1576
+ return [];
1577
+ }
1578
+ var VioCart = class extends LitElement {
1579
+ constructor() {
1580
+ super(...arguments);
1581
+ this.open = false;
1582
+ this.heading = "Handlekurv";
1583
+ this.checkoutLabel = "Til kassen";
1584
+ this.emptyLabel = "Handlekurven er tom";
1585
+ this.subtotalLabel = "Sum";
1586
+ this.carts = /* @__PURE__ */ new Map();
1587
+ this.itemCount = 0;
1588
+ this.applePayAvailable = false;
1589
+ this.confirmedOrder = null;
1590
+ /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
1591
+ this.applePayHandle = null;
1592
+ /** Cart total Apple Pay was last prepared for (avoids redundant prep). */
1593
+ this.preparedTotal = -1;
1594
+ this.boundOnCartChange = (e) => {
1595
+ const detail = e.detail;
1596
+ this.carts = detail.cartsBySponsor;
1597
+ this.itemCount = detail.itemCount;
1598
+ };
1599
+ }
1600
+ connectedCallback() {
1601
+ super.connectedCallback();
1602
+ this.carts = Vio.cart.getAllCarts();
1603
+ this.itemCount = Vio.cart.itemCount;
1604
+ Vio.cart.addEventListener("change", this.boundOnCartChange);
1605
+ }
1606
+ disconnectedCallback() {
1607
+ Vio.cart.removeEventListener("change", this.boundOnCartChange);
1608
+ super.disconnectedCallback();
1609
+ }
1610
+ close() {
1611
+ this.open = false;
1612
+ }
1613
+ show() {
1614
+ this.open = true;
1615
+ }
1616
+ updated(changed) {
1617
+ if (changed.has("open") && this.open) {
1618
+ this.confirmedOrder = null;
1619
+ this.preparedTotal = -1;
1620
+ }
1621
+ if (this.open && this.itemCount > 0) {
1622
+ const total = this.totalAcrossSponsors;
1623
+ if (total !== this.preparedTotal) {
1624
+ this.preparedTotal = total;
1625
+ void this.refreshApplePay(total);
1626
+ }
1627
+ }
1628
+ }
1629
+ /** Pre-resolve the Apple Pay sheet for the current cart total. */
1630
+ async refreshApplePay(total) {
1631
+ if (total <= 0) {
1632
+ this.applePayHandle = null;
1633
+ this.applePayAvailable = false;
1634
+ return;
1635
+ }
1636
+ try {
1637
+ const handle = await Vio.checkout.prepareApplePayExpress(total, this.primaryCurrency);
1638
+ if (this.preparedTotal !== total) return;
1639
+ this.applePayHandle = handle;
1640
+ this.applePayAvailable = handle.available;
1641
+ } catch {
1642
+ this.applePayHandle = null;
1643
+ this.applePayAvailable = false;
1644
+ }
1645
+ }
1646
+ /** Apple Pay express — pay the whole cart, then show the confirmation. */
1647
+ onApplePay() {
1648
+ const handle = this.applePayHandle;
1649
+ if (!handle || !handle.available || this.itemCount === 0) return;
1650
+ const snapshot = this.buildConfirmation();
1651
+ void handle.show().then((result) => {
1652
+ if (!result) return;
1653
+ this.confirmedOrder = snapshot;
1654
+ for (const sponsorId of [...this.carts.keys()]) {
1655
+ Vio.cart.clearSponsorCart(sponsorId);
1656
+ }
1657
+ this.dispatchEvent(
1658
+ new CustomEvent("vio:payment-success", {
1659
+ bubbles: true,
1660
+ composed: true,
1661
+ detail: { method: "apple-pay", result }
1662
+ })
1663
+ );
1664
+ });
1665
+ }
1666
+ buildConfirmation() {
1667
+ const items = [];
1668
+ for (const cart of this.carts.values()) {
1669
+ for (const it of cart.items) {
1670
+ items.push({
1671
+ brand: it.brand,
1672
+ name: it.name,
1673
+ quantity: it.quantity,
1674
+ unitPrice: it.unitPrice,
1675
+ currency: it.currency
1676
+ });
1677
+ }
1678
+ }
1679
+ return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
1680
+ }
1681
+ closeConfirmation() {
1682
+ this.confirmedOrder = null;
1683
+ this.close();
1684
+ }
1685
+ /** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
1686
+ * skipping the address form (leveringsadresse) and method picker
1687
+ * (betalingsmetode). The host's checkout-open handler closes this cart and
1688
+ * opens <vio-checkout> in express mode with Klarna preselected. */
1689
+ onKlarnaExpress() {
1690
+ if (this.itemCount === 0) return;
1691
+ const firstSponsorId = [...this.carts.keys()][0];
1692
+ if (firstSponsorId === void 0) return;
1693
+ this.dispatchEvent(
1694
+ new CustomEvent("vio:checkout-open", {
1695
+ bubbles: true,
1696
+ composed: true,
1697
+ detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
1698
+ })
1699
+ );
1700
+ }
1701
+ get totalAcrossSponsors() {
1702
+ let sum = 0;
1703
+ for (const cart of this.carts.values()) {
1704
+ for (const item of cart.items) sum += item.unitPrice * item.quantity;
1705
+ }
1706
+ return sum;
1707
+ }
1708
+ get primaryCurrency() {
1709
+ for (const cart of this.carts.values()) return cart.currency;
1710
+ return "NOK";
1711
+ }
1712
+ onIncrement(item) {
1713
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
1714
+ }
1715
+ onDecrement(item) {
1716
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
1717
+ }
1718
+ onRemove(item) {
1719
+ Vio.cart.removeItem(item.id, item.sponsorId);
1720
+ }
1721
+ render() {
1722
+ const isEmpty = this.itemCount === 0;
1723
+ const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
1724
+ return html`
1725
+ <div class="backdrop" @click=${this.close}></div>
1726
+ <aside class="drawer" role="dialog" aria-label=${this.heading}>
1727
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
1728
+ <div class="header">
1729
+ <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
1730
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1731
+ </div>
1732
+
1733
+ ${this.confirmedOrder ? this.renderConfirmation() : isEmpty ? html`<div class="empty">${this.emptyLabel}</div>` : html`
1734
+ <div class="body">
1735
+ ${sponsorCarts.map(
1736
+ (cart) => html`
1737
+ <div class="sponsor-group">
1738
+ ${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
1739
+ ${cart.items.map(
1740
+ (item) => html`
1741
+ <div class="item">
1742
+ <div class="item-image">
1743
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
1744
+ </div>
1745
+ <div>
1746
+ <div class="item-brand">${item.brand}</div>
1747
+ <div class="item-name">${item.name}</div>
1748
+ <div class="item-row">
1749
+ <span class="item-qty">
1750
+ <button
1751
+ class="qty-btn"
1752
+ @click=${() => this.onDecrement(item)}
1753
+ aria-label="Reduser"
1754
+ >
1755
+ −
1756
+ </button>
1757
+ ${item.quantity}
1758
+ <button
1759
+ class="qty-btn"
1760
+ @click=${() => this.onIncrement(item)}
1761
+ aria-label="Øk"
1762
+ >
1763
+ +
1764
+ </button>
1765
+ <button
1766
+ class="qty-btn"
1767
+ @click=${() => this.onRemove(item)}
1768
+ aria-label="Fjern"
1769
+ style="margin-left: 8px;"
1770
+ >
1771
+ ×
1772
+ </button>
1773
+ </span>
1774
+ <span class="item-price">
1775
+ ${formatPrice(
1776
+ item.unitPrice * item.quantity,
1777
+ item.currency
1778
+ )}
1779
+ </span>
1780
+ </div>
1781
+ </div>
1782
+ </div>
1783
+ `
1784
+ )}
1785
+ </div>
1786
+ `
1787
+ )}
1788
+ </div>
1789
+ <div class="footer">
1790
+ <div class="subtotal-row">
1791
+ <span class="subtotal-label">${this.subtotalLabel}</span>
1792
+ <span class="subtotal-value">
1793
+ ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
1794
+ </span>
1795
+ </div>
1796
+ ${this.applePayAvailable ? html`
1797
+ <button
1798
+ class="applepay-btn"
1799
+ @click=${this.onApplePay}
1800
+ aria-label="Betal med Apple Pay"
1801
+ >
1802
+ <span class="ap-logo">${"\uF8FF"}</span> Pay
1803
+ </button>
1804
+ ` : ""}
1805
+ <button
1806
+ class="klarna-btn"
1807
+ @click=${this.onKlarnaExpress}
1808
+ aria-label="Betal med Klarna"
1809
+ >
1810
+ Kjøp med
1811
+ <img
1812
+ class="klarna-logo"
1813
+ src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1814
+ alt="Klarna"
1815
+ />
1816
+ </button>
1817
+ </div>
1818
+ `}
1819
+ </aside>
1820
+ `;
1821
+ }
1822
+ renderConfirmation() {
1823
+ const o = this.confirmedOrder;
1824
+ if (!o) return html``;
1825
+ return html`
1826
+ <div class="confirm">
1827
+ <div class="confirm-check" aria-hidden="true">✓</div>
1828
+ <h3>Takk for handelen!</h3>
1829
+ <p>Betaling med Apple Pay er bekreftet.</p>
1830
+ <div class="confirm-summary">
1831
+ ${o.items.map(
1832
+ (it) => html`
1833
+ <div class="order-row">
1834
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
1835
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
1836
+ </div>
1837
+ `
1838
+ )}
1839
+ <div class="order-row total">
1840
+ <span>Totalt</span>
1841
+ <span>${formatPrice(o.total, o.currency)}</span>
1842
+ </div>
1843
+ </div>
1844
+ <button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
1845
+ </div>
1846
+ `;
1847
+ }
1848
+ };
1849
+ VioCart.styles = css`
1850
+ :host { display: contents; }
1851
+
1852
+ .backdrop {
1853
+ position: fixed;
1854
+ inset: 0;
1855
+ background: rgba(0, 0, 0, 0.4);
1856
+ opacity: 0;
1857
+ pointer-events: none;
1858
+ transition: opacity 0.25s ease;
1859
+ z-index: 998;
1860
+ }
1861
+ :host([open]) .backdrop { opacity: 1; pointer-events: auto; }
1862
+
1863
+ .drawer {
1864
+ position: fixed;
1865
+ top: 0;
1866
+ right: 0;
1867
+ bottom: 0;
1868
+ width: min(440px, 100%);
1869
+ background: var(--vio-color-surface, #fff);
1870
+ transform: translateX(100%);
1871
+ transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
1872
+ display: flex;
1873
+ flex-direction: column;
1874
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
1875
+ color: var(--vio-color-text, #0a0a0a);
1876
+ z-index: 999;
1877
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.06);
1878
+ }
1879
+ :host([open]) .drawer { transform: translateX(0); }
1880
+
1881
+ .handle { display: none; }
1882
+
1883
+ /* Mobile: bottom sheet (slide up, rounded top, drag handle). */
1884
+ @media (max-width: 600px) {
1885
+ .drawer {
1886
+ top: auto;
1887
+ right: 0;
1888
+ left: 0;
1889
+ bottom: 0;
1890
+ width: 100%;
1891
+ height: 88vh;
1892
+ border-radius: 16px 16px 0 0;
1893
+ transform: translateY(100%);
1894
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
1895
+ }
1896
+ :host([open]) .drawer { transform: translateY(0); }
1897
+ .handle {
1898
+ display: flex;
1899
+ justify-content: center;
1900
+ padding: 10px 0 4px;
1901
+ }
1902
+ .handle::before {
1903
+ content: '';
1904
+ width: 40px;
1905
+ height: 4px;
1906
+ background: var(--vio-color-border, #e5e5e5);
1907
+ border-radius: 2px;
1908
+ }
1909
+ }
1910
+
1911
+ .header {
1912
+ padding: var(--vio-space-lg, 24px);
1913
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
1914
+ display: flex;
1915
+ justify-content: space-between;
1916
+ align-items: center;
1917
+ }
1918
+ .heading {
1919
+ font-family: var(--vio-font-serif, Georgia, serif);
1920
+ font-size: 24px;
1921
+ font-weight: 400;
1922
+ margin: 0;
1923
+ }
1924
+ .close {
1925
+ background: none;
1926
+ border: none;
1927
+ cursor: pointer;
1928
+ font-size: 28px;
1929
+ line-height: 1;
1930
+ padding: 0;
1931
+ color: var(--vio-color-text, #0a0a0a);
1932
+ font-family: inherit;
1933
+ }
1934
+ .close:hover { color: var(--vio-color-text-secondary, #666); }
1935
+
1936
+ .body {
1937
+ flex: 1;
1938
+ overflow-y: auto;
1939
+ }
1940
+
1941
+ .sponsor-group {
1942
+ padding: var(--vio-space-md, 16px) var(--vio-space-lg, 24px);
1943
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
1944
+ }
1945
+ .sponsor-group:last-child { border-bottom: none; }
1946
+ .sponsor-label {
1947
+ font-size: 11px;
1948
+ text-transform: uppercase;
1949
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1950
+ color: var(--vio-color-text-tertiary, #999);
1951
+ font-weight: 500;
1952
+ margin: 0 0 var(--vio-space-md, 16px);
1953
+ }
1954
+
1955
+ .item {
1956
+ display: grid;
1957
+ grid-template-columns: 80px 1fr;
1958
+ gap: var(--vio-space-md, 16px);
1959
+ padding: var(--vio-space-md, 16px) 0;
1960
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
1961
+ }
1962
+ .item:last-child { border-bottom: none; }
1963
+ .item-image {
1964
+ aspect-ratio: 1;
1965
+ background: var(--vio-color-surface-muted, #f2f2f2);
1966
+ overflow: hidden;
1967
+ }
1968
+ .item-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
1969
+ .item-brand {
1970
+ font-size: 11px;
1971
+ text-transform: uppercase;
1972
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1973
+ color: var(--vio-color-text-secondary, #666);
1974
+ font-weight: 500;
1975
+ }
1976
+ .item-name {
1977
+ font-size: 14px;
1978
+ margin: 4px 0;
1979
+ font-weight: 500;
1980
+ line-height: 1.3;
1981
+ }
1982
+ .item-row {
1983
+ display: flex;
1984
+ justify-content: space-between;
1985
+ align-items: center;
1986
+ margin-top: var(--vio-space-sm, 8px);
1987
+ }
1988
+ .item-qty {
1989
+ display: flex;
1990
+ align-items: center;
1991
+ gap: 6px;
1992
+ color: var(--vio-color-text-secondary, #666);
1993
+ font-size: 12px;
1994
+ }
1995
+ .qty-btn {
1996
+ width: 24px;
1997
+ height: 24px;
1998
+ border: 1px solid var(--vio-color-border, #e5e5e5);
1999
+ background: #fff;
2000
+ border-radius: 2px;
2001
+ cursor: pointer;
2002
+ font-size: 12px;
2003
+ color: var(--vio-color-text, #0a0a0a);
2004
+ line-height: 1;
2005
+ padding: 0;
2006
+ font-family: inherit;
2007
+ }
2008
+ .qty-btn:hover { border-color: var(--vio-color-text, #0a0a0a); }
2009
+ .item-price { font-size: 14px; font-weight: 700; }
2010
+
2011
+ .empty {
2012
+ padding: 80px var(--vio-space-lg, 24px);
2013
+ text-align: center;
2014
+ color: var(--vio-color-text-secondary, #666);
2015
+ font-size: 14px;
2016
+ }
2017
+
2018
+ .footer {
2019
+ padding: var(--vio-space-lg, 24px);
2020
+ border-top: 1px solid var(--vio-color-border, #e5e5e5);
2021
+ background: var(--vio-color-surface, #fff);
2022
+ }
2023
+ .subtotal-row {
2024
+ display: flex;
2025
+ justify-content: space-between;
2026
+ margin-bottom: var(--vio-space-md, 16px);
2027
+ font-size: 14px;
2028
+ }
2029
+ .subtotal-label { color: var(--vio-color-text-secondary, #666); }
2030
+ .subtotal-value { font-weight: 700; font-size: 16px; }
2031
+
2032
+ .checkout-btn {
2033
+ width: 100%;
2034
+ padding: 18px;
2035
+ background: var(--vio-color-text, #0a0a0a);
2036
+ color: var(--vio-color-text-on-primary, #fff);
2037
+ border: none;
2038
+ font-size: 12px;
2039
+ text-transform: uppercase;
2040
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2041
+ cursor: pointer;
2042
+ font-weight: 600;
2043
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2044
+ transition: background 0.15s;
2045
+ }
2046
+ .checkout-btn:hover:not(:disabled) { background: #222; }
2047
+ .checkout-btn:disabled {
2048
+ background: var(--vio-color-text-tertiary, #999);
2049
+ cursor: not-allowed;
2050
+ }
2051
+
2052
+ /* Apple Pay express — pay the whole cart directly, above "Til kassen". */
2053
+ .applepay-btn {
2054
+ width: 100%;
2055
+ padding: 15px;
2056
+ margin-bottom: 10px;
2057
+ background: #000;
2058
+ color: #fff;
2059
+ border: none;
2060
+ border-radius: 8px;
2061
+ font-size: 19px;
2062
+ font-weight: 500;
2063
+ line-height: 1;
2064
+ cursor: pointer;
2065
+ display: flex;
2066
+ align-items: center;
2067
+ justify-content: center;
2068
+ gap: 5px;
2069
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2070
+ transition: opacity 0.15s;
2071
+ }
2072
+ .applepay-btn:hover { opacity: 0.86; }
2073
+ .applepay-btn .ap-logo { font-size: 19px; line-height: 1; }
2074
+ /* Klarna express — pink brand button, same express row as Apple Pay. */
2075
+ .klarna-btn {
2076
+ width: 100%;
2077
+ padding: 15px;
2078
+ margin-bottom: 10px;
2079
+ background: #ffb3c7;
2080
+ color: #0a0a0a;
2081
+ border: none;
2082
+ border-radius: 8px;
2083
+ font-size: 16px;
2084
+ font-weight: 600;
2085
+ cursor: pointer;
2086
+ display: flex;
2087
+ align-items: center;
2088
+ justify-content: center;
2089
+ gap: 6px;
2090
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2091
+ transition: background 0.15s;
2092
+ }
2093
+ .klarna-btn:hover { background: #ffa1ba; }
2094
+ .klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
2095
+ .express-divider {
2096
+ display: flex;
2097
+ align-items: center;
2098
+ gap: 12px;
2099
+ margin: 4px 0 12px;
2100
+ color: var(--vio-color-text-tertiary, #999);
2101
+ font-size: 11px;
2102
+ text-transform: uppercase;
2103
+ letter-spacing: 0.1em;
2104
+ }
2105
+ .express-divider::before,
2106
+ .express-divider::after {
2107
+ content: '';
2108
+ flex: 1;
2109
+ height: 1px;
2110
+ background: var(--vio-color-border, #e5e5e5);
2111
+ }
2112
+
2113
+ /* In-drawer confirmation after Apple Pay (nothing else is shown). */
2114
+ .confirm {
2115
+ padding: 56px var(--vio-space-lg, 24px) 40px;
2116
+ text-align: center;
2117
+ display: flex;
2118
+ flex-direction: column;
2119
+ align-items: center;
2120
+ }
2121
+ .confirm-check {
2122
+ width: 64px;
2123
+ height: 64px;
2124
+ border-radius: 50%;
2125
+ background: #0a8a3f;
2126
+ color: #fff;
2127
+ font-size: 34px;
2128
+ display: flex;
2129
+ align-items: center;
2130
+ justify-content: center;
2131
+ margin-bottom: 20px;
2132
+ }
2133
+ .confirm h3 {
2134
+ font-family: var(--vio-font-serif, Georgia, serif);
2135
+ font-size: 24px;
2136
+ font-weight: 400;
2137
+ margin: 0 0 8px;
2138
+ }
2139
+ .confirm p {
2140
+ color: var(--vio-color-text-secondary, #666);
2141
+ font-size: 14px;
2142
+ margin: 0 0 24px;
2143
+ }
2144
+ .confirm-summary {
2145
+ width: 100%;
2146
+ border-top: 1px solid var(--vio-color-border, #e5e5e5);
2147
+ padding-top: 16px;
2148
+ margin-bottom: 28px;
2149
+ text-align: left;
2150
+ }
2151
+ .order-row {
2152
+ display: flex;
2153
+ justify-content: space-between;
2154
+ gap: 12px;
2155
+ font-size: 14px;
2156
+ margin-bottom: 10px;
2157
+ color: var(--vio-color-text, #0a0a0a);
2158
+ }
2159
+ .order-row.total {
2160
+ border-top: 1px solid var(--vio-color-border, #e5e5e5);
2161
+ padding-top: 12px;
2162
+ margin-top: 4px;
2163
+ font-weight: 700;
2164
+ }
2165
+ .confirm-close {
2166
+ width: 100%;
2167
+ padding: 16px;
2168
+ background: var(--vio-color-text, #0a0a0a);
2169
+ color: #fff;
2170
+ border: none;
2171
+ font-size: 12px;
2172
+ text-transform: uppercase;
2173
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2174
+ cursor: pointer;
2175
+ font-weight: 600;
2176
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2177
+ }
2178
+ .confirm-close:hover { background: #222; }
2179
+ `;
2180
+ __decorateClass([
2181
+ property({ type: Boolean, reflect: true })
2182
+ ], VioCart.prototype, "open", 2);
2183
+ __decorateClass([
2184
+ property({ type: String })
2185
+ ], VioCart.prototype, "heading", 2);
2186
+ __decorateClass([
2187
+ property({ type: String, attribute: "checkout-label" })
2188
+ ], VioCart.prototype, "checkoutLabel", 2);
2189
+ __decorateClass([
2190
+ property({ type: String, attribute: "empty-label" })
2191
+ ], VioCart.prototype, "emptyLabel", 2);
2192
+ __decorateClass([
2193
+ property({ type: String, attribute: "subtotal-label" })
2194
+ ], VioCart.prototype, "subtotalLabel", 2);
2195
+ __decorateClass([
2196
+ state()
2197
+ ], VioCart.prototype, "carts", 2);
2198
+ __decorateClass([
2199
+ state()
2200
+ ], VioCart.prototype, "itemCount", 2);
2201
+ __decorateClass([
2202
+ state()
2203
+ ], VioCart.prototype, "applePayAvailable", 2);
2204
+ __decorateClass([
2205
+ state()
2206
+ ], VioCart.prototype, "confirmedOrder", 2);
2207
+ VioCart = __decorateClass([
2208
+ customElement("vio-cart")
2209
+ ], VioCart);
2210
+ var VioCheckout = class extends LitElement {
2211
+ constructor() {
2212
+ super(...arguments);
2213
+ this.open = false;
2214
+ this.heading = "Kasse";
2215
+ this.shippingLabel = "Beregnes ved kasse";
2216
+ this.express = false;
2217
+ this.checkoutState = null;
2218
+ this.items = [];
2219
+ this.applePayAvailable = false;
2220
+ this.applePayInProgress = false;
2221
+ this.klarnaAvailable = false;
2222
+ this.orderConfirmed = false;
2223
+ this.confirmedMethod = null;
2224
+ this.paymentError = null;
2225
+ this.confirmedOrder = null;
2226
+ /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2227
+ this.klarnaHandle = null;
2228
+ this.klarnaMountedAmount = null;
2229
+ this.klarnaMountedShipping = null;
2230
+ this.klarnaMounting = false;
2231
+ this.klarnaCategories = [];
2232
+ this.klarnaSelectedCat = "";
2233
+ this.klarnaAuthorizing = false;
2234
+ this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
2235
+ this.form = {
2236
+ firstName: "",
2237
+ lastName: "",
2238
+ email: "",
2239
+ address: "",
2240
+ postalCode: "",
2241
+ city: ""
2242
+ };
2243
+ this.boundOnCheckoutChange = (e) => {
2244
+ const detail = e.detail;
2245
+ this.checkoutState = detail.state;
2246
+ this.items = Vio.checkout.items;
2247
+ if (this.checkoutState) {
2248
+ this.refreshApplePay();
2249
+ this.refreshKlarna();
2250
+ } else {
2251
+ this.applePayAvailable = false;
2252
+ this.klarnaAvailable = false;
2253
+ this.orderConfirmed = false;
2254
+ this.confirmedMethod = null;
2255
+ this.confirmedOrder = null;
2256
+ this.paymentError = null;
2257
+ this.express = false;
2258
+ this.unmountKlarna();
2259
+ }
2260
+ };
2261
+ this.boundOnPaymentComplete = (e) => {
2262
+ const detail = e.detail;
2263
+ if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
2264
+ const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
2265
+ if (sponsorId == null) return;
2266
+ this.confirmOrder(detail.method, sponsorId, detail.result);
2267
+ };
2268
+ this.boundOnPaymentError = (e) => {
2269
+ const detail = e.detail;
2270
+ this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
2271
+ this.open = true;
2272
+ };
2273
+ }
2274
+ connectedCallback() {
2275
+ super.connectedCallback();
2276
+ this.checkoutState = Vio.checkout.currentState;
2277
+ this.items = Vio.checkout.items;
2278
+ Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
2279
+ Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
2280
+ Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
2281
+ if (this.checkoutState) {
2282
+ void this.refreshApplePay();
2283
+ void this.refreshKlarna();
2284
+ }
2285
+ }
2286
+ disconnectedCallback() {
2287
+ Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
2288
+ Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
2289
+ Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
2290
+ this.unmountKlarna();
2291
+ super.disconnectedCallback();
2292
+ }
2293
+ updated() {
2294
+ void this.mountKlarnaIfNeeded();
2295
+ }
2296
+ close() {
2297
+ this.open = false;
2298
+ this.express = false;
2299
+ Vio.checkout.close();
2300
+ }
2301
+ show() {
2302
+ this.open = true;
2303
+ }
2304
+ async refreshApplePay() {
2305
+ Vio.checkout.clearApplePayCache();
2306
+ this.applePayAvailable = await Vio.checkout.isApplePayAvailable("NO");
2307
+ }
2308
+ async refreshKlarna() {
2309
+ const available = await Vio.checkout.klarnaPaymentsAvailable();
2310
+ if (available !== this.klarnaAvailable) {
2311
+ if (!available) this.unmountKlarna();
2312
+ this.klarnaAvailable = available;
2313
+ }
2314
+ }
2315
+ /** Mount (or re-mount on amount change) the Klarna Payments widget. */
2316
+ async mountKlarnaIfNeeded() {
2317
+ if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
2318
+ if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
2319
+ if (this.klarnaMounting) return;
2320
+ const amount = this.checkoutState.subtotal;
2321
+ if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
2322
+ return;
2323
+ }
2324
+ const container = this.renderRoot?.querySelector(
2325
+ "#vio-klarna-payments-container"
2326
+ );
2327
+ if (!container) return;
2328
+ this.klarnaMounting = true;
2329
+ this.unmountKlarna();
2330
+ container.innerHTML = "";
2331
+ try {
2332
+ const handle = await Vio.checkout.mountKlarnaPayments(container, {
2333
+ withShipping: this.express,
2334
+ shippingId: this.selectedShipping
2335
+ });
2336
+ this.klarnaHandle = handle;
2337
+ this.klarnaMountedAmount = amount;
2338
+ this.klarnaMountedShipping = this.selectedShipping;
2339
+ this.klarnaCategories = handle.categories;
2340
+ this.klarnaSelectedCat = handle.selected;
2341
+ } catch (err) {
2342
+ if (typeof console !== "undefined") {
2343
+ console.warn("[VioCheckout] Klarna Payments mount failed:", err);
2344
+ }
2345
+ this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
2346
+ } finally {
2347
+ this.klarnaMounting = false;
2348
+ }
2349
+ }
2350
+ unmountKlarna() {
2351
+ if (this.klarnaHandle) {
2352
+ this.klarnaHandle.unmount();
2353
+ this.klarnaHandle = null;
2354
+ }
2355
+ this.klarnaMountedAmount = null;
2356
+ this.klarnaMountedShipping = null;
2357
+ this.klarnaCategories = [];
2358
+ this.klarnaSelectedCat = "";
2359
+ this.klarnaAuthorizing = false;
2360
+ }
2361
+ /** Selected shipping option (express flow). */
2362
+ get shippingOption() {
2363
+ return KLARNA_SHIPPING_OPTIONS.find((o) => o.id === this.selectedShipping) ?? null;
2364
+ }
2365
+ /** Pick a shipping option → re-mounts the Klarna widget with the new total. */
2366
+ onSelectShipping(id) {
2367
+ if (this.selectedShipping === id || this.klarnaMounting) return;
2368
+ this.selectedShipping = id;
2369
+ }
2370
+ /** Switch the Klarna widget to a different payment_method_category. */
2371
+ async onKlarnaCategory(identifier) {
2372
+ if (!this.klarnaHandle || this.klarnaSelectedCat === identifier) return;
2373
+ this.klarnaSelectedCat = identifier;
2374
+ try {
2375
+ await this.klarnaHandle.load(identifier);
2376
+ } catch (err) {
2377
+ if (typeof console !== "undefined") {
2378
+ console.warn("[VioCheckout] Klarna category switch failed:", err);
2379
+ }
2380
+ }
2381
+ }
2382
+ /** Authorize the selected Klarna method → backend creates the order. */
2383
+ async onKlarnaAuthorize() {
2384
+ if (!this.klarnaHandle || this.klarnaAuthorizing) return;
2385
+ this.paymentError = null;
2386
+ this.klarnaAuthorizing = true;
2387
+ try {
2388
+ await this.klarnaHandle.authorize();
2389
+ } finally {
2390
+ this.klarnaAuthorizing = false;
2391
+ }
2392
+ }
2393
+ onFieldChange(field, value) {
2394
+ this.form = { ...this.form, [field]: value };
2395
+ Vio.checkout.setAddress(this.form);
2396
+ }
2397
+ async onApplePay() {
2398
+ if (this.applePayInProgress) return;
2399
+ this.applePayInProgress = true;
2400
+ try {
2401
+ await Vio.checkout.startApplePay({ country: "NO", label: "Vio" });
2402
+ } catch (err) {
2403
+ if (typeof console !== "undefined") {
2404
+ console.warn("[VioCheckout] Apple Pay flow ended:", err);
2405
+ }
2406
+ } finally {
2407
+ this.applePayInProgress = false;
2408
+ }
2409
+ }
2410
+ onPay(method) {
2411
+ this.paymentError = null;
2412
+ if (method === "apple-pay") {
2413
+ void this.onApplePay();
2414
+ return;
2415
+ }
2416
+ Vio.checkout.selectPaymentMethod(method);
2417
+ }
2418
+ /**
2419
+ * Complete the order for the selected method (Vipps / Card / plain Klarna).
2420
+ * Apple Pay and Klarna Express finish inside their own sheet/popup, so they
2421
+ * never reach here. No real PSP is wired for Vipps/Card in this iteration —
2422
+ * the capture happens backend-side in the full flow — so this confirms the
2423
+ * order state-only and shows the confirmation screen.
2424
+ */
2425
+ onCompleteOrder() {
2426
+ const s = this.checkoutState;
2427
+ if (!s || !s.paymentMethod) return;
2428
+ this.confirmOrder(s.paymentMethod, s.sponsorId);
2429
+ }
2430
+ /**
2431
+ * Shared order confirmation — reached from both the plain "Betal" CTA
2432
+ * (Vipps/Card/Klarna) and the Klarna Express authorize callback. Shows
2433
+ * the confirmation screen, emits `vio:payment-success`, and clears the
2434
+ * paid sponsor's cart so the badge + drawer reflect the purchase.
2435
+ */
2436
+ confirmOrder(method, sponsorId, result) {
2437
+ const r = result ?? {};
2438
+ this.confirmedOrder = {
2439
+ items: [...this.items],
2440
+ currency: this.checkoutState?.currency ?? "",
2441
+ total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
2442
+ orderId: r.order?.orderId
2443
+ };
2444
+ this.confirmedMethod = method;
2445
+ this.orderConfirmed = true;
2446
+ this.open = true;
2447
+ this.dispatchEvent(
2448
+ new CustomEvent("vio:payment-success", {
2449
+ bubbles: true,
2450
+ composed: true,
2451
+ detail: { method, sponsorId, result }
2452
+ })
2453
+ );
2454
+ Vio.cart.clearSponsorCart(sponsorId);
2455
+ }
2456
+ methodLabel(method) {
2457
+ switch (method) {
2458
+ case "apple-pay":
2459
+ return "Apple Pay";
2460
+ case "klarna":
2461
+ return "Klarna";
2462
+ case "vipps":
2463
+ return "Vipps";
2464
+ case "card":
2465
+ return "kort";
2466
+ default:
2467
+ return "";
2468
+ }
2469
+ }
2470
+ /** True for methods that complete via a separate sheet/widget, not the CTA. */
2471
+ isExpressMethod(method) {
2472
+ return method === "apple-pay" || method === "klarna";
2473
+ }
2474
+ orderTotal() {
2475
+ const s = this.checkoutState;
2476
+ if (!s) return "";
2477
+ return formatPrice(s.subtotal, s.currency);
2478
+ }
2479
+ /** Shipping cost (major units) applied in express mode, else 0. */
2480
+ shippingMajor() {
2481
+ return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
2482
+ }
2483
+ /** Total to charge — items + shipping (express). */
2484
+ payTotalLabel() {
2485
+ const s = this.checkoutState;
2486
+ if (!s) return "";
2487
+ return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
2488
+ }
2489
+ render() {
2490
+ return html`
2491
+ <div class="backdrop" @click=${this.close}></div>
2492
+ <div
2493
+ class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
2494
+ role="dialog"
2495
+ aria-label=${this.heading}
2496
+ >
2497
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
2498
+ <div class="topbar">
2499
+ <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
2500
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
2501
+ </div>
2502
+
2503
+ <div class="body">
2504
+ ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
2505
+ </div>
2506
+ </div>
2507
+ `;
2508
+ }
2509
+ renderConfirmation() {
2510
+ const o = this.confirmedOrder;
2511
+ return html`
2512
+ <section class="confirmation">
2513
+ <div class="confirm-check" aria-hidden="true">✓</div>
2514
+ <h3 class="confirm-title">Takk for bestillingen!</h3>
2515
+ <p class="confirm-text">
2516
+ Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
2517
+ </p>
2518
+ ${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
2519
+ ${o && o.items.length > 0 ? html`
2520
+ <div class="confirm-summary">
2521
+ ${o.items.map(
2522
+ (it) => html`
2523
+ <div class="order-row">
2524
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
2525
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
2526
+ </div>
2527
+ `
2528
+ )}
2529
+ <div class="order-row total">
2530
+ <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
2531
+ </div>
2532
+ </div>
2533
+ ` : ""}
2534
+ <button class="confirm-close" @click=${this.close}>Lukk</button>
2535
+ </section>
2536
+ `;
2537
+ }
2538
+ /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
2539
+ renderKlarnaPanel() {
2540
+ const currency = this.checkoutState?.currency ?? "NOK";
2541
+ return html`
2542
+ <div class="klarna-panel">
2543
+ ${this.express ? html`
2544
+ <div class="ship-select" role="radiogroup" aria-label="Frakt">
2545
+ ${KLARNA_SHIPPING_OPTIONS.map(
2546
+ (o) => html`
2547
+ <button
2548
+ class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
2549
+ role="radio"
2550
+ aria-checked=${this.selectedShipping === o.id}
2551
+ ?disabled=${this.klarnaMounting}
2552
+ @click=${() => this.onSelectShipping(o.id)}
2553
+ >
2554
+ <span class="ship-meta">
2555
+ <b>${o.method}</b><span>${o.description}</span>
2556
+ </span>
2557
+ <span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
2558
+ </button>
2559
+ `
2560
+ )}
2561
+ </div>
2562
+ ` : ""}
2563
+ ${this.klarnaCategories.length > 1 ? html`
2564
+ <div class="klarna-cats">
2565
+ ${this.klarnaCategories.map(
2566
+ (c) => html`
2567
+ <button
2568
+ class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
2569
+ @click=${() => void this.onKlarnaCategory(c.identifier)}
2570
+ >
2571
+ ${c.name}
2572
+ </button>
2573
+ `
2574
+ )}
2575
+ </div>
2576
+ ` : ""}
2577
+ <div class="klarna-widget" id="vio-klarna-payments-container"></div>
2578
+ <button
2579
+ class="payment-btn primary complete-cta"
2580
+ @click=${() => void this.onKlarnaAuthorize()}
2581
+ ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
2582
+ >
2583
+ ${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
2584
+ </button>
2585
+ </div>
2586
+ `;
2587
+ }
2588
+ /** Express layout — only the Klarna widget + a compact order summary. */
2589
+ renderKlarnaExpress() {
2590
+ return html`
2591
+ <section class="section">
2592
+ <h3 class="section-heading">Kjøp med Klarna</h3>
2593
+ ${this.items.length > 0 ? html`
2594
+ <div class="express-summary">
2595
+ ${this.items.map(
2596
+ (item) => html`
2597
+ <div class="order-row">
2598
+ <span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
2599
+ <span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
2600
+ </div>
2601
+ `
2602
+ )}
2603
+ <div class="order-row">
2604
+ <span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
2605
+ <span>${formatPrice(this.shippingMajor(), this.checkoutState?.currency ?? "NOK")}</span>
2606
+ </div>
2607
+ <div class="order-row total">
2608
+ <span>Totalt</span><span>${this.payTotalLabel()}</span>
2609
+ </div>
2610
+ </div>
2611
+ ` : ""}
2612
+ ${this.renderKlarnaPanel()}
2613
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2614
+ </section>
2615
+ `;
2616
+ }
2617
+ renderCheckoutBody() {
2618
+ const method = this.checkoutState?.paymentMethod;
2619
+ const showCompleteCta = !!method && !this.isExpressMethod(method);
2620
+ return html`
2621
+ <section class="section">
2622
+ <div class="section-label">Steg 1</div>
2623
+ <h3 class="section-heading">Leveringsadresse</h3>
2624
+ <div class="form-row">
2625
+ <div class="field">
2626
+ <label>Fornavn</label>
2627
+ <input
2628
+ type="text"
2629
+ autocomplete="given-name"
2630
+ .value=${this.form.firstName}
2631
+ @input=${(e) => this.onFieldChange("firstName", e.target.value)}
2632
+ />
2633
+ </div>
2634
+ <div class="field">
2635
+ <label>Etternavn</label>
2636
+ <input
2637
+ type="text"
2638
+ autocomplete="family-name"
2639
+ .value=${this.form.lastName}
2640
+ @input=${(e) => this.onFieldChange("lastName", e.target.value)}
2641
+ />
2642
+ </div>
2643
+ </div>
2644
+ <div class="field">
2645
+ <label>E-post</label>
2646
+ <input
2647
+ type="email"
2648
+ autocomplete="email"
2649
+ .value=${this.form.email}
2650
+ @input=${(e) => this.onFieldChange("email", e.target.value)}
2651
+ />
2652
+ </div>
2653
+ <div class="field">
2654
+ <label>Adresse</label>
2655
+ <input
2656
+ type="text"
2657
+ autocomplete="street-address"
2658
+ .value=${this.form.address}
2659
+ @input=${(e) => this.onFieldChange("address", e.target.value)}
2660
+ />
2661
+ </div>
2662
+ <div class="form-row">
2663
+ <div class="field">
2664
+ <label>Postnummer</label>
2665
+ <input
2666
+ type="text"
2667
+ autocomplete="postal-code"
2668
+ .value=${this.form.postalCode}
2669
+ @input=${(e) => this.onFieldChange("postalCode", e.target.value)}
2670
+ />
2671
+ </div>
2672
+ <div class="field">
2673
+ <label>Sted</label>
2674
+ <input
2675
+ type="text"
2676
+ autocomplete="address-level2"
2677
+ .value=${this.form.city}
2678
+ @input=${(e) => this.onFieldChange("city", e.target.value)}
2679
+ />
2680
+ </div>
2681
+ </div>
2682
+ </section>
2683
+
2684
+ <section class="section">
2685
+ <div class="section-label">Steg 2</div>
2686
+ <h3 class="section-heading">Velg betalingsmåte</h3>
2687
+ <div class="payment-grid">
2688
+ ${this.applePayAvailable ? html`
2689
+ <button
2690
+ class="payment-btn primary"
2691
+ @click=${() => this.onPay("apple-pay")}
2692
+ ?disabled=${this.applePayInProgress}
2693
+ aria-pressed=${this.checkoutState?.paymentMethod === "apple-pay"}
2694
+ >
2695
+ ${this.applePayInProgress ? "\xC5pner\u2026" : " Pay"}
2696
+ </button>
2697
+ ` : ""}
2698
+ ${this.klarnaAvailable ? html`
2699
+ <button
2700
+ class="payment-btn"
2701
+ @click=${() => this.onPay("klarna")}
2702
+ aria-pressed=${this.checkoutState?.paymentMethod === "klarna"}
2703
+ >
2704
+ Klarna
2705
+ </button>
2706
+ ` : ""}
2707
+ <button
2708
+ class="payment-btn"
2709
+ @click=${() => this.onPay("vipps")}
2710
+ aria-pressed=${this.checkoutState?.paymentMethod === "vipps"}
2711
+ >
2712
+ Vipps
2713
+ </button>
2714
+ <button
2715
+ class="payment-btn"
2716
+ @click=${() => this.onPay("card")}
2717
+ aria-pressed=${this.checkoutState?.paymentMethod === "card"}
2718
+ >
2719
+ Kort
2720
+ </button>
2721
+ ${!this.applePayAvailable ? html`
2722
+ <div class="apple-pay-note">
2723
+ Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
2724
+ </div>
2725
+ ` : ""}
2726
+ ${showCompleteCta ? html`
2727
+ <button
2728
+ class="payment-btn primary complete-cta"
2729
+ @click=${this.onCompleteOrder}
2730
+ >
2731
+ Betal ${this.orderTotal()} med ${this.methodLabel(method ?? null)}
2732
+ </button>
2733
+ ` : ""}
2734
+ ${method === "klarna" ? this.renderKlarnaPanel() : ""}
2735
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2736
+ </div>
2737
+ </section>
2738
+
2739
+ ${this.items.length > 0 ? html`
2740
+ <section class="order-summary">
2741
+ ${this.items.map(
2742
+ (item) => html`
2743
+ <div class="order-line">
2744
+ <div class="order-line-img">
2745
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
2746
+ </div>
2747
+ <div class="order-line-meta">
2748
+ <span>${item.brand}</span>
2749
+ <b>${item.name}</b>
2750
+ <span>×${item.quantity}</span>
2751
+ </div>
2752
+ <div class="order-line-price">
2753
+ ${formatPrice(item.unitPrice * item.quantity, item.currency)}
2754
+ </div>
2755
+ </div>
2756
+ `
2757
+ )}
2758
+ <div class="order-row" style="margin-top: 16px;">
2759
+ <span>Sum</span><span>${this.orderTotal()}</span>
2760
+ </div>
2761
+ <div class="order-row">
2762
+ <span>Frakt</span><span>${this.shippingLabel}</span>
2763
+ </div>
2764
+ <div class="order-row total">
2765
+ <span>Totalt</span><span>${this.orderTotal()}</span>
2766
+ </div>
2767
+ </section>
2768
+ ` : ""}
2769
+ </div>
2770
+ </div>
2771
+ `;
2772
+ }
2773
+ };
2774
+ VioCheckout.styles = css`
2775
+ :host { display: contents; }
2776
+ .backdrop {
2777
+ position: fixed;
2778
+ inset: 0;
2779
+ background: rgba(0, 0, 0, 0.5);
2780
+ opacity: 0;
2781
+ pointer-events: none;
2782
+ transition: opacity 0.25s ease;
2783
+ z-index: 999;
2784
+ }
2785
+ :host([open]) .backdrop { opacity: 1; pointer-events: auto; }
2786
+
2787
+ .modal {
2788
+ position: fixed;
2789
+ inset: 0;
2790
+ background: var(--vio-color-surface, #fff);
2791
+ transform: translateY(100%);
2792
+ transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
2793
+ display: flex;
2794
+ flex-direction: column;
2795
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2796
+ color: var(--vio-color-text, #0a0a0a);
2797
+ z-index: 1000;
2798
+ overflow-y: auto;
2799
+ }
2800
+ :host([open]) .modal { transform: translateY(0); }
2801
+
2802
+ /* Confirmation drawer — compact bottom sheet, not the full-screen overlay. */
2803
+ .modal.as-drawer {
2804
+ inset: auto 0 0 0;
2805
+ height: auto;
2806
+ max-height: 88vh;
2807
+ max-width: 460px;
2808
+ margin: 0 auto;
2809
+ border-radius: 16px 16px 0 0;
2810
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
2811
+ }
2812
+ .modal.as-drawer .handle {
2813
+ display: flex;
2814
+ justify-content: center;
2815
+ padding: 10px 0 4px;
2816
+ flex-shrink: 0;
2817
+ }
2818
+ .modal.as-drawer .handle::before {
2819
+ content: '';
2820
+ width: 40px;
2821
+ height: 4px;
2822
+ background: var(--vio-color-border, #e5e5e5);
2823
+ border-radius: 2px;
2824
+ }
2825
+
2826
+ /* Express (Apple Pay-style): right-side drawer, like the cart. Slides in
2827
+ from the right instead of the full-screen overlay. */
2828
+ .modal.as-side {
2829
+ inset: 0 0 0 auto;
2830
+ width: min(440px, 100%);
2831
+ max-width: 440px;
2832
+ transform: translateX(100%);
2833
+ border-radius: 0;
2834
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2835
+ }
2836
+ :host([open]) .modal.as-side { transform: translateX(0); }
2837
+ @media (max-width: 600px) {
2838
+ /* On phones, keep it a bottom sheet for reachability. */
2839
+ .modal.as-side {
2840
+ inset: auto 0 0 0;
2841
+ width: 100%;
2842
+ max-width: none;
2843
+ height: 92vh;
2844
+ transform: translateY(100%);
2845
+ border-radius: 16px 16px 0 0;
2846
+ }
2847
+ :host([open]) .modal.as-side { transform: translateY(0); }
2848
+ }
2849
+
2850
+ /* Confirmation: a centered desktop dialog (the bottom-sheet looked like
2851
+ mobile on desktop). Falls back to a bottom sheet on phones. */
2852
+ .modal.as-confirm {
2853
+ inset: auto;
2854
+ top: 50%;
2855
+ left: 50%;
2856
+ width: min(92vw, 440px);
2857
+ max-width: 440px;
2858
+ height: auto;
2859
+ max-height: 88vh;
2860
+ border-radius: 16px;
2861
+ box-shadow: 0 30px 80px -24px rgba(0, 0, 0, 0.4);
2862
+ transform: translate(-50%, -50%) scale(0.96);
2863
+ opacity: 0;
2864
+ }
2865
+ :host([open]) .modal.as-confirm {
2866
+ transform: translate(-50%, -50%) scale(1);
2867
+ opacity: 1;
2868
+ }
2869
+ @media (max-width: 600px) {
2870
+ .modal.as-confirm {
2871
+ inset: auto 0 0 0;
2872
+ top: auto;
2873
+ left: 0;
2874
+ width: 100%;
2875
+ max-width: none;
2876
+ border-radius: 16px 16px 0 0;
2877
+ transform: translateY(100%) scale(1);
2878
+ opacity: 1;
2879
+ }
2880
+ :host([open]) .modal.as-confirm { transform: translateY(0); }
2881
+ }
2882
+
2883
+ .handle { display: none; }
2884
+
2885
+ /* Mobile: bottom sheet with rounded top + drag handle. */
2886
+ @media (max-width: 600px) {
2887
+ .modal {
2888
+ top: auto;
2889
+ height: 92vh;
2890
+ border-radius: 16px 16px 0 0;
2891
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
2892
+ }
2893
+ .handle {
2894
+ display: flex;
2895
+ justify-content: center;
2896
+ padding: 10px 0 4px;
2897
+ flex-shrink: 0;
2898
+ }
2899
+ .handle::before {
2900
+ content: '';
2901
+ width: 40px;
2902
+ height: 4px;
2903
+ background: var(--vio-color-border, #e5e5e5);
2904
+ border-radius: 2px;
2905
+ }
2906
+ }
2907
+
2908
+ .topbar {
2909
+ display: flex;
2910
+ justify-content: space-between;
2911
+ align-items: center;
2912
+ padding: 20px var(--vio-space-lg, 24px);
2913
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
2914
+ position: sticky;
2915
+ top: 0;
2916
+ background: var(--vio-color-surface, #fff);
2917
+ z-index: 1;
2918
+ }
2919
+ .heading {
2920
+ font-family: var(--vio-font-serif, Georgia, serif);
2921
+ font-size: 24px;
2922
+ font-weight: 400;
2923
+ margin: 0;
2924
+ }
2925
+ .close {
2926
+ background: none;
2927
+ border: none;
2928
+ cursor: pointer;
2929
+ font-size: 28px;
2930
+ line-height: 1;
2931
+ padding: 0;
2932
+ color: var(--vio-color-text, #0a0a0a);
2933
+ font-family: inherit;
2934
+ }
2935
+ .close:hover { color: var(--vio-color-text-secondary, #666); }
2936
+
2937
+ .body {
2938
+ max-width: 720px;
2939
+ margin: 0 auto;
2940
+ width: 100%;
2941
+ padding: 56px var(--vio-space-lg, 24px) 80px;
2942
+ box-sizing: border-box;
2943
+ }
2944
+ .section { margin-bottom: 56px; }
2945
+ .section-label {
2946
+ font-size: 12px;
2947
+ text-transform: uppercase;
2948
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2949
+ color: var(--vio-color-accent, #c14a3b);
2950
+ margin-bottom: 12px;
2951
+ font-weight: 500;
2952
+ }
2953
+ .section-heading {
2954
+ font-family: var(--vio-font-serif, Georgia, serif);
2955
+ font-size: 28px;
2956
+ margin: 0 0 var(--vio-space-lg, 24px);
2957
+ font-weight: 400;
2958
+ line-height: 1.2;
2959
+ }
2960
+
2961
+ .form-row {
2962
+ display: grid;
2963
+ grid-template-columns: 1fr 1fr;
2964
+ gap: var(--vio-space-md, 16px);
2965
+ }
2966
+ .field {
2967
+ display: flex;
2968
+ flex-direction: column;
2969
+ gap: 6px;
2970
+ margin-bottom: var(--vio-space-md, 16px);
2971
+ }
2972
+ .field label {
2973
+ font-size: 11px;
2974
+ text-transform: uppercase;
2975
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2976
+ color: var(--vio-color-text-secondary, #666);
2977
+ font-weight: 500;
2978
+ }
2979
+ .field input {
2980
+ padding: 12px 0;
2981
+ border: none;
2982
+ border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
2983
+ font-size: 16px;
2984
+ font-family: inherit;
2985
+ color: inherit;
2986
+ background: transparent;
2987
+ outline: none;
2988
+ transition: border-color 0.15s;
2989
+ }
2990
+ .field input:focus { border-bottom-color: var(--vio-color-text, #0a0a0a); }
2991
+
2992
+ .payment-grid {
2993
+ display: grid;
2994
+ grid-template-columns: 1fr 1fr;
2995
+ gap: var(--vio-space-md, 16px);
2996
+ }
2997
+ .payment-btn {
2998
+ padding: 20px;
2999
+ background: #fff;
3000
+ border: 1px solid var(--vio-color-border, #e5e5e5);
3001
+ cursor: pointer;
3002
+ font-family: inherit;
3003
+ font-size: 14px;
3004
+ font-weight: 500;
3005
+ display: flex;
3006
+ align-items: center;
3007
+ justify-content: center;
3008
+ gap: 8px;
3009
+ transition: border-color 0.15s, background 0.15s;
3010
+ color: var(--vio-color-text, #0a0a0a);
3011
+ }
3012
+ .payment-btn:hover:not(:disabled) {
3013
+ border-color: var(--vio-color-text, #0a0a0a);
3014
+ background: var(--vio-color-surface-hover, #fafafa);
3015
+ }
3016
+ .payment-btn:disabled {
3017
+ cursor: progress;
3018
+ opacity: 0.6;
3019
+ }
3020
+ .payment-btn.primary {
3021
+ background: var(--vio-color-text, #0a0a0a);
3022
+ color: var(--vio-color-text-on-primary, #fff);
3023
+ grid-column: 1 / -1;
3024
+ border-color: var(--vio-color-text, #0a0a0a);
3025
+ padding: 22px;
3026
+ font-weight: 600;
3027
+ }
3028
+ .payment-btn.primary:hover:not(:disabled) { background: #222; border-color: #222; }
3029
+ .payment-btn[aria-pressed='true'] {
3030
+ border-color: var(--vio-color-accent, #c14a3b);
3031
+ }
3032
+ .apple-pay-note {
3033
+ grid-column: 1 / -1;
3034
+ font-size: 12px;
3035
+ color: var(--vio-color-text-tertiary, #999);
3036
+ text-align: center;
3037
+ margin-top: 4px;
3038
+ }
3039
+ /* Klarna Payments — inline widget panel, shown once Klarna is selected. */
3040
+ .klarna-panel {
3041
+ grid-column: 1 / -1;
3042
+ display: flex;
3043
+ flex-direction: column;
3044
+ gap: 12px;
3045
+ margin-top: 4px;
3046
+ }
3047
+ .klarna-cats {
3048
+ display: flex;
3049
+ flex-wrap: wrap;
3050
+ gap: 8px;
3051
+ }
3052
+ .klarna-cat {
3053
+ flex: 1 1 auto;
3054
+ padding: 8px 12px;
3055
+ border: 1px solid var(--vio-color-border, #ddd);
3056
+ border-radius: 8px;
3057
+ background: #fff;
3058
+ font-size: 13px;
3059
+ cursor: pointer;
3060
+ }
3061
+ .klarna-cat.active {
3062
+ border-color: var(--vio-color-text, #111);
3063
+ background: var(--vio-color-surface-2, #f6f6f6);
3064
+ font-weight: 600;
3065
+ }
3066
+ .klarna-widget {
3067
+ min-height: 48px;
3068
+ }
3069
+ .express-summary {
3070
+ display: flex;
3071
+ flex-direction: column;
3072
+ gap: 6px;
3073
+ margin-bottom: 16px;
3074
+ }
3075
+ /* Shipping selector (express) — Standard / Express options. */
3076
+ .ship-select { display: flex; flex-direction: column; gap: 8px; }
3077
+ .ship-opt {
3078
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
3079
+ padding: 12px 14px; border: 1px solid var(--vio-color-border, #ddd);
3080
+ border-radius: 10px; background: #fff; cursor: pointer; font-family: inherit;
3081
+ text-align: left;
3082
+ }
3083
+ .ship-opt.active {
3084
+ border-color: var(--vio-color-text, #111);
3085
+ background: var(--vio-color-surface-2, #f6f6f6);
3086
+ }
3087
+ .ship-opt:disabled { opacity: 0.6; cursor: default; }
3088
+ .ship-meta { display: flex; flex-direction: column; gap: 2px; }
3089
+ .ship-meta b { font-weight: 600; font-size: 13px; }
3090
+ .ship-meta span { color: var(--vio-color-text-secondary, #666); font-size: 12px; }
3091
+ .ship-price { font-weight: 600; font-size: 13px; }
3092
+ .payment-error {
3093
+ grid-column: 1 / -1;
3094
+ padding: 12px 16px;
3095
+ background: #fdecea;
3096
+ border: 1px solid var(--vio-color-accent, #c14a3b);
3097
+ border-radius: 8px;
3098
+ font-size: 13px;
3099
+ color: var(--vio-color-accent, #c14a3b);
3100
+ text-align: center;
3101
+ }
3102
+ /* "Betal" CTA — sits full-width below the method grid once a method is
3103
+ picked. Reuses .payment-btn.primary (its grid-column spans the row). */
3104
+ .complete-cta { margin-top: 8px; }
3105
+
3106
+ /* Order confirmation — compact, shown inside the drawer. */
3107
+ .confirmation {
3108
+ text-align: center;
3109
+ padding: 24px 24px 28px;
3110
+ display: flex;
3111
+ flex-direction: column;
3112
+ align-items: center;
3113
+ gap: 14px;
3114
+ }
3115
+ .confirm-check {
3116
+ width: 64px;
3117
+ height: 64px;
3118
+ border-radius: 50%;
3119
+ background: #2e7d32;
3120
+ color: #fff;
3121
+ display: flex;
3122
+ align-items: center;
3123
+ justify-content: center;
3124
+ font-size: 32px;
3125
+ line-height: 1;
3126
+ }
3127
+ .confirm-title {
3128
+ font-family: var(--vio-font-serif, Georgia, serif);
3129
+ font-size: 28px;
3130
+ font-weight: 400;
3131
+ margin: 0;
3132
+ }
3133
+ .confirm-text {
3134
+ font-size: 14px;
3135
+ color: var(--vio-color-text-secondary, #666);
3136
+ max-width: 360px;
3137
+ line-height: 1.6;
3138
+ margin: 0;
3139
+ }
3140
+ .confirm-order-id {
3141
+ font-size: 13px;
3142
+ color: var(--vio-color-text-secondary, #666);
3143
+ }
3144
+ .confirm-order-id b {
3145
+ color: var(--vio-color-text, #0a0a0a);
3146
+ font-variant-numeric: tabular-nums;
3147
+ }
3148
+ .confirm-summary {
3149
+ width: 100%;
3150
+ max-width: 360px;
3151
+ border-top: 1px solid var(--vio-color-border, #eee);
3152
+ padding-top: 14px;
3153
+ display: flex;
3154
+ flex-direction: column;
3155
+ gap: 8px;
3156
+ text-align: left;
3157
+ font-size: 13px;
3158
+ }
3159
+ .confirm-close {
3160
+ border: none;
3161
+ background: var(--vio-color-text, #0a0a0a);
3162
+ color: var(--vio-color-text-on-primary, #fff);
3163
+ padding: 16px 48px;
3164
+ font-size: 12px;
3165
+ text-transform: uppercase;
3166
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3167
+ font-weight: 600;
3168
+ cursor: pointer;
3169
+ font-family: inherit;
3170
+ margin-top: 16px;
3171
+ }
3172
+ .confirm-close:hover { background: #222; }
3173
+
3174
+ .order-summary {
3175
+ background: var(--vio-color-surface-muted, #f2f2f2);
3176
+ padding: var(--vio-space-lg, 24px);
3177
+ margin-top: var(--vio-space-xl, 32px);
3178
+ }
3179
+ .order-line {
3180
+ display: grid;
3181
+ grid-template-columns: 56px 1fr auto;
3182
+ gap: 12px;
3183
+ padding: 8px 0;
3184
+ align-items: center;
3185
+ }
3186
+ .order-line-img {
3187
+ aspect-ratio: 1;
3188
+ background: #fff;
3189
+ overflow: hidden;
3190
+ }
3191
+ .order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
3192
+ .order-line-meta { font-size: 13px; }
3193
+ .order-line-meta b { display: block; }
3194
+ .order-line-meta span {
3195
+ color: var(--vio-color-text-secondary, #666);
3196
+ font-size: 11px;
3197
+ text-transform: uppercase;
3198
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3199
+ }
3200
+ .order-line-price { font-weight: 700; font-size: 14px; }
3201
+ .order-row {
3202
+ display: flex;
3203
+ justify-content: space-between;
3204
+ padding: 8px 0;
3205
+ font-size: 14px;
3206
+ }
3207
+ .order-row.total {
3208
+ border-top: 1px solid var(--vio-color-border-default, #ccc);
3209
+ margin-top: var(--vio-space-sm, 8px);
3210
+ padding-top: var(--vio-space-md, 16px);
3211
+ font-weight: 700;
3212
+ font-size: 16px;
3213
+ }
3214
+
3215
+ @media (max-width: 600px) {
3216
+ .form-row { grid-template-columns: 1fr; gap: 0; }
3217
+ .payment-grid { grid-template-columns: 1fr; }
3218
+ .body { padding: 32px var(--vio-space-lg, 24px) 64px; }
3219
+ }
3220
+ `;
3221
+ __decorateClass([
3222
+ property({ type: Boolean, reflect: true })
3223
+ ], VioCheckout.prototype, "open", 2);
3224
+ __decorateClass([
3225
+ property({ type: String })
3226
+ ], VioCheckout.prototype, "heading", 2);
3227
+ __decorateClass([
3228
+ property({ type: String, attribute: "shipping-label" })
3229
+ ], VioCheckout.prototype, "shippingLabel", 2);
3230
+ __decorateClass([
3231
+ property({ type: Boolean, reflect: true })
3232
+ ], VioCheckout.prototype, "express", 2);
3233
+ __decorateClass([
3234
+ state()
3235
+ ], VioCheckout.prototype, "checkoutState", 2);
3236
+ __decorateClass([
3237
+ state()
3238
+ ], VioCheckout.prototype, "items", 2);
3239
+ __decorateClass([
3240
+ state()
3241
+ ], VioCheckout.prototype, "applePayAvailable", 2);
3242
+ __decorateClass([
3243
+ state()
3244
+ ], VioCheckout.prototype, "applePayInProgress", 2);
3245
+ __decorateClass([
3246
+ state()
3247
+ ], VioCheckout.prototype, "klarnaAvailable", 2);
3248
+ __decorateClass([
3249
+ state()
3250
+ ], VioCheckout.prototype, "orderConfirmed", 2);
3251
+ __decorateClass([
3252
+ state()
3253
+ ], VioCheckout.prototype, "confirmedMethod", 2);
3254
+ __decorateClass([
3255
+ state()
3256
+ ], VioCheckout.prototype, "paymentError", 2);
3257
+ __decorateClass([
3258
+ state()
3259
+ ], VioCheckout.prototype, "confirmedOrder", 2);
3260
+ __decorateClass([
3261
+ state()
3262
+ ], VioCheckout.prototype, "klarnaCategories", 2);
3263
+ __decorateClass([
3264
+ state()
3265
+ ], VioCheckout.prototype, "klarnaSelectedCat", 2);
3266
+ __decorateClass([
3267
+ state()
3268
+ ], VioCheckout.prototype, "klarnaAuthorizing", 2);
3269
+ __decorateClass([
3270
+ state()
3271
+ ], VioCheckout.prototype, "selectedShipping", 2);
3272
+ __decorateClass([
3273
+ state()
3274
+ ], VioCheckout.prototype, "form", 2);
3275
+ VioCheckout = __decorateClass([
3276
+ customElement("vio-checkout")
3277
+ ], VioCheckout);
3278
+
3279
+ // src/ui/index.ts
3280
+ var VIO_UI_VERSION = "0.1.0";
3281
+
3282
+ export { VIO_UI_VERSION, VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens };
3283
+ //# sourceMappingURL=chunk-4CCGE7DE.js.map
3284
+ //# sourceMappingURL=chunk-4CCGE7DE.js.map