@vio-live/web-sdk 0.26.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,289 @@
1
+ # Changelog — `@vio-live/web-sdk`
2
+
3
+ What changed in each version, and **what a host has to know before upgrading**.
4
+ Every version has up to four parts:
5
+
6
+ - **Added** — new things you can use.
7
+ - **Changed** — the same feature behaves differently (screens, events, data on the wire).
8
+ - **Fixed** — bugs that are gone.
9
+ - **⚠️ Upgrade notes** — what a host (Vev, Replit, a custom page) must do, check or
10
+ watch for. Read these before bumping, even for a patch.
11
+
12
+ Newest first. `Unreleased` is merged on `main` but **not published to npm** yet.
13
+ How to keep this file: see [CONTRIBUTING → Changelog](CONTRIBUTING.md#changelog).
14
+ The details of each feature live in the [README](README.md); this file says *what*
15
+ and *when*, and links there.
16
+
17
+ ## Unreleased
18
+
19
+ Nothing yet.
20
+
21
+ ## 0.28.0 — 2026-10-22
22
+
23
+ ### Added
24
+ - **The detail's layout is public**: 31 new `applyVioTheme` keys, each a `--vio-*` property
25
+ (logo height and zoom, image fit / aspect / max height / zoom / side margin, thumbnail size and gap, option
26
+ height and gap, button height and radius, a sticky buy zone, cart / checkout widths,
27
+ confirmation logo and title, the redirect overlay), plus `::part` names on every piece of
28
+ the detail and the confirmation. The same keys serve our demo, Replit and Vev's Config block.
29
+ See [README → Layout](README.md#layout-of-the-detail-cart-checkout-and-confirmation-028).
30
+ - **The hand-over to Vipps**: a full-screen «Åpner Vipps …» with a soft fade from the tap to
31
+ the Vipps page (detail, cart, checkout), which also stops a second tap; events
32
+ `vio:payment-redirect-start` (cancelable — draw your own loader), `vio:payment-redirect-error`
33
+ and `vio:payment-redirect-cancel`. See [README → The hand-over to Vipps](README.md#the-hand-over-to-vipps-028).
34
+ - `commerce.checkout_id` on every `purchase` the SDK can tie to a checkout, and
35
+ `props.order_id_kind` (`'order'` | `'checkout'`).
36
+ - `vio:payment-success` may carry `orderNumber` when the checkout already knows it.
37
+ - `purchaseIdsOf(detail)` → `{ orderId, checkoutId, orderIdKind }` (from `/core`) for
38
+ hosts with their own analytics sink.
39
+
40
+ ### Changed
41
+ - **Product detail** (asked by Mote & Livsstil): the selling brand's logo (its name without
42
+ one; a square logo with the name beside it) sits **top left beside ✕**, no longer above the
43
+ image; every main image shows in **one 4:5 box it fills** (`cover`) — no white bars, no jump
44
+ between photos of different shapes; thumbnails are **one row** (swipe, arrows with a mouse,
45
+ the selected one kept in view) instead of wrapping; sizes are **compact** (36px); **Vipps
46
+ comes first**, above quantity and «Legg i handlekurv»; the **description folds** («Beskrivelse»
47
+ opens on a tap); under the buy buttons, **«Du handler direkte hos {brand}.»**, with links to the
48
+ brand's «Kjøpsvilkår» and «Angrerett og retur» when its sponsor block carries `termsUrl` /
49
+ `refundPolicyUrl` (https only; `Vio.sponsorIdentity()` returns them too). On desktop the
50
+ description comes before the buy zone, which stays pinned at the bottom of the panel; phones
51
+ keep buy zone, then description.
52
+ - The confirmation's brand logo is 40px tall (was max 32px).
53
+
54
+ ### Fixed
55
+ - **Phones: the detail, the cart and the checkout close by pulling them down**, from the grab bar
56
+ or the top bar (logo / title and ✕): the sheet follows the finger, and closes when let go far
57
+ enough or with a flick, or springs back. Before, the grab bar only answered a tap — pulling it
58
+ scrolled the sheet. The checkout never closes this way mid-payment.
59
+ - **Phones: the page behind stays put while a sheet is open.** A swipe past the end of the
60
+ detail, cart or checkout used to scroll the article underneath. The page is locked while any
61
+ sheet is open and comes back to the same place when the last one closes. Desktop drawers are
62
+ unchanged.
63
+ - **The `purchase` event's `commerce.order_id` is Vio's order number** (`4415`), not the
64
+ checkout's uuid. Before, every purchase paid through a redirect (Vipps, Klarna, hosted
65
+ Stripe) or whose order a webhook creates carried the uuid, so Commerce could never net
66
+ a cancellation or refund (`order_cancelled` / `order_refunded`, ADR-0036) against it.
67
+ ([#88](https://github.com/vio-live/vio-web-sdk/pull/88))
68
+
69
+ ### ⚠️ Upgrade notes
70
+ - **The product detail looks different** after upgrading (see *Changed*). A host that styled
71
+ `.sponsor-header` / `.topbar-brand` from outside: they are gone — use `::part(sponsor)`.
72
+ Hosts that want the old look back: `detailImageAspect: '1 / 1'`, `detailImageFit: 'contain'`,
73
+ `detailOptionHeight: '42px'`, `detailBuyPosition: 'static'`. The `4 / 5` box crops photos of
74
+ other shapes: pick the proportion that fits the catalog, or `contain`.
75
+ - A host with its own «loading Vipps» screen: call `preventDefault()` on
76
+ `vio:payment-redirect-start`, or the SDK's overlay shows on top of yours.
77
+ - **The `purchase` may leave up to 10 s after `vio:payment-success`**: the SDK asks the
78
+ checkout for the order number at once and then after 0.5, 1, 2, 3 and 3 s. The confirmation screen, the
79
+ cart and the payment never wait — only the analytics event. If the page hides first,
80
+ the event leaves at once with the checkout id (`order_id_kind: 'checkout'`).
81
+ - **Hosts with their own sink (Vev):** read the ids with `purchaseIdsOf(detail)` and wait
82
+ for the number the same way (`Vio.checkout.getOrderReference(checkoutId)`), or your
83
+ purchases keep the uuid.
84
+ - **Dashboards / queries** that joined purchases by checkout uuid: use `checkout_id`.
85
+ - **Hosts that lock the page themselves when a Vio sheet opens** (phones): the SDK does it now —
86
+ `position: fixed` on `<body>` at the current offset, its own inline styles restored and the
87
+ scroll put back on close. Drop yours, or make sure it does not undo the body's styles while a
88
+ sheet is still open.
89
+ - One `purchase` per checkout per page load: a second `vio:payment-success` for the same
90
+ checkout adds nothing.
91
+ - **«Kjøpsvilkår · Angrerett og retur» need the backend**: the links show only when the
92
+ sponsor block carries `termsUrl` / `refundPolicyUrl`, which the campaigns service sends
93
+ once the brand fills them in (Settings → Brand → Shopper policies). Until then the
94
+ sentence «Du handler direkte hos {brand}.» shows alone. Nothing to do on the host.
95
+
96
+ ## 0.27.0 — 2026-10-21
97
+
98
+ ### Added
99
+ - **The brand that sells is named** — name and logo — in the product detail (top),
100
+ the checkout header («Du handler hos …») and the confirmation («Du har handlet hos
101
+ …»), also after a Vipps return and a reload. ([#91](https://github.com/vio-live/vio-web-sdk/pull/91))
102
+ - `Vio.sponsorIdentity(sponsorId, { checkoutId? })` → `{ id, name, logoUrl, avatarUrl } | null`.
103
+ - `vio:checkout-open` and `vio:payment-success` carry `sponsor: { id, name, logoUrl }`.
104
+ - `Vio.checkout.getOrderReference(checkoutId, sponsorId?)` also answers `sponsor`.
105
+ - `sponsorIdentityFrom(bootstrap, sponsorId)` exported from `/core`.
106
+
107
+ ### Changed
108
+ - The detail's top line names the seller only once (no repeated seller name).
109
+ - The checkout remembers, per tab, which sponsor each checkout belongs to
110
+ (`sessionStorage` `vio.checkoutSponsor.v1`, last 20 checkouts).
111
+
112
+ ### ⚠️ Upgrade notes
113
+ - The sponsor in the checkout and its confirmation is **the checkout's** (the cart's
114
+ brand), never the article's: an article of one brand can sell another brand's product.
115
+ - A host confirmation page that must survive a reload keeps `checkoutId` and `sponsorId`
116
+ in its own URL and asks `getOrderReference` again: the SDK cleans its return
117
+ parameters after using them. Recipe in [README → Sponsor identity](README.md#sponsor-identity--name-and-logo).
118
+ - New marks are styled with `--vio-*` tokens and exposed as `part="sponsor"`.
119
+
120
+ ## 0.26.1 — 2026-10-21
121
+
122
+ ### Fixed
123
+ - A surface with **no active campaign** still names its channel sponsor in the
124
+ attribution (before, the sponsor came out empty). ([#89](https://github.com/vio-live/vio-web-sdk/pull/89))
125
+
126
+ ## 0.26.0 — 2026-10-20
127
+
128
+ ### Added
129
+ - **The order is born attributed** (ADR-0035 B): every `AddItem` / quantity increase
130
+ carries the line's `origin` (`campaign_id`, slot, `component_kind`, `content_url`,
131
+ `content_title`), and `CreateCheckout` / the last `UpdateCheckout` carry the checkout's
132
+ `attribution`. See [README → Attribution on the order](README.md#attribution-on-the-order--what-commerce-receives-026).
133
+
134
+ ### Changed
135
+ - `content_url` is sent **without query string or fragment**.
136
+ - Privacy mode `minimal` sends neither the page nor the slot.
137
+ - The surface's privacy mode is preloaded, so the first events already respect it.
138
+ - `vio:payment-success` never waits longer than the page stays open; one reference
139
+ request per checkout.
140
+ - The landing `referrer` has no query and is dropped when it is a self-referral;
141
+ `buyer_id` only with consent.
142
+
143
+ ### ⚠️ Upgrade notes
144
+ - **Against a gateway without the new fields** (`origin` on `LineItemInput` / `UpdateItem`,
145
+ `attribution` on the checkout) the SDK resends the operation without them once per page
146
+ load and says so in the console: the cart works as in 0.25, **but the order is not
147
+ attributed**. Deploy the backend for the attribution to reach Commerce.
148
+ - Hosts that add to the cart with their own button should pass
149
+ `origin: Vio.attribution(slot)` (or `e.detail.origin`) to `Vio.cart.addProduct`.
150
+
151
+ ## 0.25.0 — 2026-10-19
152
+
153
+ ### Added
154
+ - The checkout asks the gateway for **`buyer_ref`** (a keyed hash of the buyer's email)
155
+ and puts it on `vio:payment-success` → analytics `buyer_id`.
156
+ - Every event carries a **`client`** block: language, the landing's referrer and utm
157
+ parameters, screen size and time zone (see [README → The visit and the buyer](README.md#the-visit-and-the-buyer--client-and-buyer_id-025)).
158
+ - Slot manifests carry `kind`; every element carries `component_template_id`.
159
+
160
+ ### Changed
161
+ - A slot assigned `page` counts as the page's campaign when its sponsor is known.
162
+
163
+ ### ⚠️ Upgrade notes
164
+ - Never store an email from the events: `buyerRef` is the reference to keep.
165
+ - With `requireConsent`, nothing is written (landing memory, ids) until `setConsent(true)`,
166
+ and `buyer_id` is not sent.
167
+
168
+ ## 0.24.0 — 2026-10-16
169
+
170
+ ### Added
171
+ - **The page says which campaign it belongs to** (ADR-0034, amendment 3):
172
+ `Vio.init({ campaignId })`, `<body data-vio-campaign="…">`, or `vio-campaign` on one
173
+ element. See [README → Page campaign](README.md#page-campaign--an-editorial-page-that-belongs-to-one-campaign).
174
+
175
+ ### Fixed
176
+ - The cart's reconcile skips the mutation that is answering; card context per product;
177
+ title and URL capped to the collector's limits.
178
+
179
+ ### ⚠️ Upgrade notes
180
+ - The attributes are read **when an element mounts**: in an SPA, update `data-vio-campaign`
181
+ (or call `Vio.init` again) before mounting the next article's blocks.
182
+ - A paused, unknown or non-numeric campaign falls through to the next level.
183
+
184
+ ## 0.23.1 — 2026-10-16
185
+
186
+ ### Added
187
+ - **Automatic slot identity**: with `data-vio-scope` (and `data-vio-scope-name`) on an
188
+ ancestor, every element registers itself (`article-123-carousel-1`); `Vio.attribution()`.
189
+ - The cart remembers the origin of each unit; a purchase is split per campaign.
190
+
191
+ ### Fixed
192
+ - Collector limits respected; a one-campaign purchase is one slice; stable origins.
193
+
194
+ ## 0.23.0 — 2026-10-16
195
+
196
+ ### Added
197
+ - **Slots**: the page declares its slots and each one resolves its campaign from the
198
+ dashboard (ADR-0027 / 0034). See [README → Slots](README.md#slots--where-things-happen-on-a-page).
199
+
200
+ ### Fixed
201
+ - The checkout waits for pending cart mutations before creating a checkout or a payment link.
202
+ - GraphQL cart calls time out after 15 s.
203
+
204
+ ### ⚠️ Upgrade notes
205
+ - `Vio.init({ platform: 'web' | 'custom-web' | 'vev' })` — default `'web'`. Only that
206
+ platform's assignments come back.
207
+
208
+ ## 0.22.0 — 2026-10-16
209
+ Version bump only; same code as 0.21.1.
210
+
211
+ ## 0.21.1 — 2026-10-16
212
+
213
+ ### Fixed
214
+ - Quantity changes are serialized and coalesced per cart line: quick taps on + / − end on
215
+ the quantity the shopper sees, with one request in flight at a time.
216
+
217
+ ## 0.21.0 — 2026-10-15
218
+
219
+ ### Fixed
220
+ - Analytics: `order_id` only with Vio ids, the cart's `value`, and `checkout_id` on
221
+ payment returns.
222
+
223
+ ## 0.20.5 — 2026-10-15
224
+
225
+ ### Fixed
226
+ - Analytics: the real `campaign_id`, a consistent `order_id` on `purchase`, and
227
+ `X-Api-Key` on the events request.
228
+
229
+ ## 0.20.4 — 2026-10-14
230
+
231
+ ### Fixed
232
+ - A bootstrap request started before a re-init no longer lands in the new configuration.
233
+
234
+ ## 0.20.3 — 2026-10-12
235
+
236
+ ### Changed
237
+ - A cart already paid in another tab reads «already paid», not «failed».
238
+
239
+ ## 0.20.1 / 0.20.2 — 2026-10-12
240
+
241
+ ### Fixed
242
+ - The dialogs' focus trap keeps keyboard focus inside in Safari, and pulls it back when the
243
+ browser walks out of the dialog.
244
+
245
+ ## 0.20.0 — 2026-10-09
246
+
247
+ ### Added
248
+ - Keyboard and focus in the detail, cart and checkout: focus moves in on open, Tab stays
249
+ inside, Esc closes, focus returns on close.
250
+
251
+ ### Changed
252
+ - The detail and the cart read the corner radius from the theme (`--vio-radius-*`).
253
+ - When a payment cannot start, the shopper is told what to do (empty cart, cart changed in
254
+ another tab, already paid) instead of a raw error.
255
+
256
+ ## 0.19.0 — 2026-10-08
257
+
258
+ ### Changed
259
+ - The product detail's header shows the seller's **Company name** (Vio Commerce →
260
+ Settings → Company), the brand when there is none. *(Superseded in 0.27.0 by the
261
+ sponsor's name and logo.)*
262
+
263
+ ### ⚠️ Upgrade notes
264
+ - The name is read with its own query outside the product cache (`useCache: false`): a
265
+ narrow query written to the gateway's product cache would hand the next product query a
266
+ product without price or images.
267
+
268
+ ## 0.18.0 — 2026-10-08
269
+
270
+ ### Added
271
+ - Vipps' **official button** in the detail, the cart and the checkout; **Vipps Express**
272
+ from the product and the cart (the app collects address and delivery).
273
+ - Stripe Connect in the embedded payment and Apple Pay (ADR-0022).
274
+
275
+ ### Changed
276
+ - Back from a payment page the drawer shows **only** «Bekrefter betalingen…» until it
277
+ knows the outcome — never the checkout behind it.
278
+ - The receipt's «Ordrenummer» is **Vio's order number** (`#4497`), never the checkout's uuid.
279
+
280
+ ### ⚠️ Upgrade notes
281
+ - Payment returns come back to **the same URL** with `vio_payment`, `vio_method`,
282
+ `vio_sponsor`, `checkout_id`: keep `<vio-checkout>` mounted on every route and do not
283
+ strip the query string on load.
284
+ - The Vipps button loads `cdn.vippsmobilepay.com`: allow it in a CSP.
285
+
286
+ ## 0.17.0 and earlier
287
+ Not recorded here — see the git history and the [releases](https://github.com/vio-live/vio-web-sdk/commits/main).
288
+ Highlights: Stripe on our own page (0.16), payment method first (0.15), Kustom (0.14),
289
+ Adyen (0.13), Nexi (0.12), analytics with consent (0.6 → 0.11).
package/README.md CHANGED
@@ -453,6 +453,113 @@ confirmation screen never waits. A host with its own checkout passes `buyerRef`
453
453
  (`requireConsent` until `setConsent(true)`) `buyer_id` is not sent at all: a visit
454
454
  measured with ephemeral ids carries no stable identifier of a person.
455
455
 
456
+ ### The purchase — `order_id` is Vio's order number, `checkout_id` beside it
457
+
458
+ A `purchase`'s **`commerce.order_id` is Vio's order number** — `4415`, the number
459
+ on the receipt, in the dashboard and in Commerce. Commerce's cancellations and
460
+ refunds (`order_cancelled` / `order_refunded`, ADR-0036) name the purchase they
461
+ compensate by that number: a purchase that carried the checkout's uuid instead
462
+ could never be netted.
463
+
464
+ - **Where the number comes from.** The `vio:payment-success` detail's
465
+ `orderNumber` (the SDK's checkout adds it when its ask for the buyer reference
466
+ already found the order), Apple Pay's backend confirmation
467
+ (`result.confirmation.orderId`), or an `orderId` that is a whole number (a host
468
+ passing the number; a checkout id is a uuid, never one). A redirect return
469
+ (Vipps, Klarna, hosted Stripe…) or an order a webhook creates usually fires the
470
+ event before the order exists: the SDK then **asks the checkout** —
471
+ `Vio.checkout.getOrderReference(checkoutId)`, the receipt's own path — at once
472
+ and after 0.5 / 1 / 2 / 3 / 3 s, for at most 10 s. Only the event waits: the
473
+ confirmation screen, the cart and the payment flow never do. Apple Pay and the
474
+ embedded Stripe Element (the drawer waits for the webhook's order before it
475
+ confirms) have the number at once.
476
+ - **`commerce.checkout_id`** — Vio's checkout id (uuid) — is always there when the
477
+ SDK knows the checkout (every SDK path, Apple Pay from the cart included): the
478
+ second key the collector can match a purchase by. It is new and additive; a
479
+ collector that predates it drops the key (its schema strips unknown fields) and
480
+ keeps the event.
481
+ - **`props.order_id_kind`** says what `order_id` is: `'order'` (the number) or
482
+ `'checkout'` — the number did not come within the 10 s, the page was hidden or
483
+ closed while waiting (the event leaves at once, by beacon), or the page has no
484
+ checkout manager to ask; then `order_id` is the checkout id, as before.
485
+ - **Once per checkout.** One `purchase` per checkout (or order) per page load: a
486
+ second `vio:payment-success` for the same one adds nothing, and a number that
487
+ arrives after the event left with the checkout id adds no second event. `ts`
488
+ and `event_id` are fixed at the payment, whenever the event leaves; a purchase
489
+ split per campaign waits once and every slice carries the same number.
490
+ - **A host with its own sink** (Vev) reads the same ids from the detail with
491
+ `purchaseIdsOf(detail)` → `{ orderId, checkoutId, orderIdKind }` (or
492
+ `purchaseFromPaymentSuccess(detail)`, exported from `/core`), and waits for the
493
+ number the same way through `Vio.checkout.getOrderReference(checkoutId)`.
494
+
495
+ ## Sponsor identity — name and logo
496
+
497
+ The elements say **who sells**: the sponsor's (brand's) display name and logo.
498
+
499
+ | Where | What it shows |
500
+ |---|---|
501
+ | `<vio-product-detail>` | a small brand header at the top of the detail — the logo (the name as its `alt`), or the name — for the sponsor the product was fetched with |
502
+ | `<vio-checkout>` header | «Du handler hos ‹logo \| name›» — the checkout's sponsor («Du har handlet hos …» over a provider's own receipt) |
503
+ | `<vio-checkout>` confirmation | «Takk for bestillingen! Du har handlet hos Careless.» next to the order number, with the logo above when there is one |
504
+ | `<vio-cart>` / carousel | the sponsor label over a sponsor's lines, the `retailer` line of a carousel's cards (by name) |
505
+
506
+ No logo → the name. A logo that fails to load → the name. Nothing known → nothing
507
+ (the product detail shows no header; the confirmation keeps «Takk for bestillingen!»
508
+ without the sentence). The marks are styled with the `--vio-*` tokens and exposed as
509
+ `part="sponsor"` (`vio-checkout::part(sponsor)`).
510
+
511
+ **One rule.** `Vio.sponsorIdentity(sponsorId, { checkoutId? })` answers
512
+ `{ id, name, logoUrl, avatarUrl } | null` — and every element uses it:
513
+
514
+ 1. the live bootstrap's sponsor blocks: the config's `primarySponsor` and
515
+ `secondarySponsors`, then every `campaigns[]` entry's primary and secondaries,
516
+ first match (a surface with no active campaign still names its channel brands);
517
+ 2. when the bootstrap does not know the id (not loaded yet, a redirect return): what
518
+ this tab remembered of the checkout — when the SDK creates a checkout it keeps
519
+ `checkoutId → { sponsorId, name, logoUrl }` in `sessionStorage`
520
+ (`vio.checkoutSponsor.v1`, the last 20 checkouts; it works, for that page,
521
+ without storage) — then the latest remembered checkout of that sponsor;
522
+ 3. the cart's `sponsorName`;
523
+ 4. `null`.
524
+
525
+ With only `{ checkoutId }` the sponsor is the one remembered for that checkout. URLs
526
+ are kept as given; an empty `logoUrl` is `null`. `sponsorIdentityFrom(bootstrap,
527
+ sponsorId)` is the pure step 1, exported from `/core`.
528
+
529
+ **From the checkout, never from the page.** In the checkout and on its confirmation
530
+ the sponsor is the **checkout's** — the cart's sponsor, the one the order is with —
531
+ from the checkout state or the return URL (`vio_sponsor`, `checkout_id`), never
532
+ `Vio.attribution()`: an article of one brand may sell another brand's product. Back
533
+ from Vipps the confirmation names the brand from the memory at once, or from the
534
+ bootstrap as soon as it lands (the thanks is shown first, the name follows).
535
+
536
+ **For hosts.** `vio:checkout-open` and `vio:payment-success` carry
537
+ `sponsor: { id, name, logoUrl }` in their detail when it can be named (the key is
538
+ absent otherwise), and `Vio.checkout.getOrderReference(checkoutId, sponsorId?)`
539
+ answers `{ orderNumber, buyerRef, sponsor? }` by the same rule. The SDK cleans its
540
+ return parameters from the URL once it has used them, so its own confirmation does
541
+ not come back on a reload — a host that wants a confirmation page that survives one
542
+ keeps the two ids in its own URL and asks again:
543
+
544
+ ```ts
545
+ // On the purchase: keep what the confirmation needs in YOUR url.
546
+ document.addEventListener('vio:payment-success', (e) => {
547
+ const d = (e as CustomEvent).detail
548
+ const checkoutId = String(d.checkoutId ?? d.orderId ?? '')
549
+ history.replaceState(null, '', `/takk?checkout=${checkoutId}&sponsor=${d.sponsorId ?? ''}`)
550
+ render(d.sponsor)
551
+ })
552
+
553
+ // On /takk (also after a reload, or in another browser):
554
+ const params = new URLSearchParams(location.search)
555
+ const ref = await Vio.checkout.getOrderReference(params.get('checkout') ?? '', Number(params.get('sponsor')) || undefined)
556
+ render(ref.sponsor, ref.orderNumber) // waits for Vio.init (up to 2 s) and the bootstrap; never throws
557
+
558
+ function render(sponsor?: { name: string; logoUrl: string | null }, orderNumber?: number | null) {
559
+ // «Takk for bestillingen! Du har handlet hos {sponsor.name}.» — the logo when sponsor.logoUrl
560
+ }
561
+ ```
562
+
456
563
  ## Theming
457
564
 
458
565
  Every component reads its colors and fonts from `--vio-*` CSS custom properties
@@ -490,6 +597,83 @@ useful if you show multiple differently-branded sponsors on the same page:
490
597
  applyVioTheme({ colorAccent: '#ff6600' }, document.getElementById('sponsor-2'))
491
598
  ```
492
599
 
600
+ ### Layout of the detail, cart, checkout and confirmation (0.28)
601
+
602
+ Beyond colors and fonts, the layout a host needs to fit its design is public too —
603
+ **one key in `applyVioTheme`, one `--vio-*` property behind it**. Our demo, Replit and
604
+ Vev's Config block set the same keys, so a page looks the same in the editor, the
605
+ preview and published. Set them with `applyVioTheme` (preferred) or as CSS on `:root`
606
+ or on the element (`vio-product-detail { --vio-detail-logo-height: 48px; }`).
607
+
608
+ | `applyVioTheme` key | Property | Default | What it does |
609
+ |---|---|---|---|
610
+ | `detailWidth` | `--vio-detail-width` | `440px` | Detail panel width (desktop) |
611
+ | `detailPaddingX` | `--vio-detail-padding-x` | `24px` | Side padding of the detail |
612
+ | `detailLogoHeight` | `--vio-detail-logo-height` | `44px` | Selling brand's logo, top left beside ✕ (width keeps its proportions; the name when there is no logo; a square logo — a mark — gets the name beside it) |
613
+ | `detailLogoZoom` | `--vio-detail-logo-zoom` | `1` | Zoom inside the logo's box: `1.8` crops the white around a mark in a square file |
614
+ | `detailTitleSize` / `detailPriceSize` | `--vio-detail-title-size` / `--vio-detail-price-size` | `26px` / `24px` | Name and price |
615
+ | `detailImageFit` | `--vio-detail-image-fit` | `cover` | `cover` = fills the box, crops the overflow; `contain` = whole image, bars where shapes differ |
616
+ | `detailImageAspect` | `--vio-detail-image-aspect` | `4 / 5` | One proportion for every image, so the panel never jumps: `1 / 1`, `3 / 4`… or `auto` (each image its own) |
617
+ | `detailImageMaxHeight` | `--vio-detail-image-max-height` | `85vh` | Cap on the main image's height |
618
+ | `detailImageZoom` | `--vio-detail-image-zoom` | `1` | Zoom; e.g. `1.15` crops a white margin baked into the file |
619
+ | `detailImageInset` | `--vio-detail-image-inset` | the padding | Side margin of the image; `0` = edge to edge |
620
+ | `detailImageBg` | `--vio-detail-image-bg` | muted surface | Behind the image |
621
+ | `detailThumbSize` / `detailThumbGap` | `--vio-detail-thumb-size` / `--vio-detail-thumb-gap` | `64px` / `8px` | Thumbnails — always one row: swipe on touch, arrows with a mouse, the selected one kept in view |
622
+ | `detailThumbRadius` / `detailThumbActiveColor` | `--vio-detail-thumb-radius` / `--vio-detail-thumb-active-color` | `0` / text color | Thumbnail corners, selected border |
623
+ | `detailOptionHeight` / `detailOptionGap` | `--vio-detail-option-height` / `--vio-detail-option-gap` | `36px` / `6px` | Size / option buttons (height is also their min width) |
624
+ | `detailOptionRadius` / `detailOptionWrap` | `--vio-detail-option-radius` / `--vio-detail-option-wrap` | `0` / `wrap` | `nowrap` = one line that scrolls sideways |
625
+ | `detailButtonHeight` / `detailButtonRadius` | `--vio-detail-button-height` / `--vio-detail-button-radius` | `52px` / `0` | Quantity stepper and «Legg i handlekurv» |
626
+ | `detailBuyPosition` / `detailBuyShadow` | `--vio-detail-buy-position` / `--vio-detail-buy-shadow` | phones `static`, desktop `sticky` | Whether the buy zone (Vipps, quantity, add to cart) stays at the bottom of the panel while scrolling |
627
+ | `cartWidth` / `checkoutWidth` | `--vio-cart-width` / `--vio-checkout-width` | `440px` | Drawer widths (desktop) |
628
+ | `confirmLogoHeight` / `confirmTitleSize` / `confirmCheckBg` | `--vio-confirm-logo-height` / `--vio-confirm-title-size` / `--vio-confirm-check-bg` | `40px` / `24px` / green | The confirmation |
629
+ | `redirectBg` / `redirectSpinner` | `--vio-redirect-bg` / `--vio-redirect-spinner` | translucent white / Vipps orange | The «Åpner Vipps …» overlay |
630
+
631
+ Also settable as CSS only: `--vio-detail-logo-max-width` (`220px`), `--vio-detail-image-position`,
632
+ `--vio-detail-image-radius`, `--vio-detail-thumb-fit` (`cover`), `--vio-detail-thumb-border-width`,
633
+ `--vio-detail-option-padding-x` (`12px`), `--vio-detail-option-font-size`, `--vio-detail-buy-gap`,
634
+ `--vio-detail-buy-padding`, `--vio-detail-buy-bg`, `--vio-detail-info-gap`, `--vio-detail-gallery-gap`,
635
+ `--vio-detail-title-weight`, `--vio-detail-price-weight`, `--vio-confirm-logo-max-width`,
636
+ `--vio-confirm-title-weight`, `--vio-confirm-text-size`, `--vio-redirect-color`, `--vio-redirect-font`,
637
+ `--vio-detail-seller-note-size` (`12px`), `--vio-detail-seller-note-align` (`center`).
638
+
639
+ **`::part` names**, for what the properties do not cover:
640
+
641
+ - `<vio-product-detail>`: `topbar`, `sponsor`, `close`, `gallery`, `image`, `thumbs`, `thumb`
642
+ (+ `thumb-active`), `thumbs-arrow`, `info`, `brand`, `title`, `price`, `stock`,
643
+ `option-group`, `option-label`, `options`, `option` (+ `option-selected`), `buy`,
644
+ `buy-vipps`, `quantity-row`, `add-to-cart`, `seller-note`, `seller-terms`, `seller-refund`,
645
+ `description`.
646
+ - `<vio-checkout>`: `sponsor`, `confirmation`, `confirm-title`, `confirm-sponsor`, `confirm-details`.
647
+
648
+ ```css
649
+ vio-product-detail::part(add-to-cart) { letter-spacing: 0; text-transform: none; }
650
+ vio-product-detail::part(option-selected) { background: #2b2b2b; }
651
+ ```
652
+
653
+ **The detail's order:** the selling brand top left; the main image; the thumbnails;
654
+ name, price, stock, sizes; then, **on phones**, the buy zone — **Vipps first** (when the
655
+ channel has it), quantity + «Legg i handlekurv», then the other ways to pay, and under them
656
+ «Du handler direkte hos {brand}.» (the product's brand, never the article's) with links to
657
+ the brand's «Kjøpsvilkår» and «Angrerett og retur» when it gave them (the sponsor block's
658
+ `termsUrl` / `refundPolicyUrl`, https only, opened in a new tab) — and the
659
+ description; **on desktop**, the description first and the buy zone pinned at the bottom
660
+ of the panel. The description folds: «Beskrivelse» opens on a tap.
661
+
662
+ ### The hand-over to Vipps (0.28)
663
+
664
+ Between the tap on Vipps (detail, cart or checkout) and the Vipps page, the SDK shows
665
+ a full-screen «Åpner Vipps …» with a soft fade — no empty moment, and it takes every
666
+ tap, so a second payment cannot start. Events on `document`:
667
+
668
+ | Event | Detail | When |
669
+ |---|---|---|
670
+ | `vio:payment-redirect-start` | `{ method, sponsorId, source }` | At the tap. **Cancelable**: `preventDefault()` and the SDK draws no overlay — draw your own |
671
+ | `vio:payment-redirect-error` | `… , reason` | It could not start. The overlay goes; the checkout opens with Vipps preselected |
672
+ | `vio:payment-redirect-cancel` | `… , reason` | `'express-off'` / `'not-started'`: the checkout opens instead (no error). `'back'`: the shopper came back with the browser's back button. `'cancelled'`: back through Vipps' own cancel (with `checkoutId`) |
673
+
674
+ `source` is `'detail'`, `'cart'` or `'checkout'`. The overlay is `.vio-redirect-overlay`
675
+ in the light DOM.
676
+
493
677
  ## Headless / SSR
494
678
 
495
679
  The root and `/core` entries export the framework-agnostic core with **no DOM side-effects** — SSR-safe and **tree-shakeable** (importing one helper pulls only that helper, ~0.5 kB):
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=chunk-67QESEV2.js.map
3
+ //# sourceMappingURL=chunk-67QESEV2.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-NCP76AXW.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-67QESEV2.js"}