notify-me-wl 3.6.0 → 3.6.2
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/README.md +227 -411
- package/build/notify-me-wl.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,146 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
# Notify Me / Coming Soon widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A drop-in widget that lets shoppers register interest in an out-of-stock size ("Notify Me") or an upcoming product ("Coming Soon"). It opens as a modal or a slide-in panel, collects an email (and optionally more), and sends it to The Wishlist's Customer Interest API.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Full integration guide: [TWC docs — Notify Me & Coming Soon](https://the-wishlist-documentation.gitbook.io/the-wishlist-documentation-docs/notify-me-and-coming-soon/overview)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Contents:** [Quick start](#quick-start) · [Product data](#product-data) · [Opening the widget](#opening-the-widget) · [Configuration](#configuration) · [Copy](#copy) · [Styling](#styling) · [Authentication](#authentication) · [Recommendations](#recommendations) · [What gets sent](#what-gets-sent) · [Development](#development) · [Upgrading](#upgrading)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
site is the primary reference for integration, the Customer Interest API, and
|
|
13
|
-
related guides.
|
|
11
|
+
## Quick start
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
- Clean, centered modal that inherits the host store's font and stays out of the theme's way (all styles are namespaced under `#twc-nm-overlay`).
|
|
17
|
-
- Dynamically display a labeled form to collect user information (email, mobile, first/last name, country, province).
|
|
18
|
-
- Adjust form copy and button text based on the widget type (`notify-me` or `coming-soon`).
|
|
19
|
-
- Inline success/error feedback (no browser `alert`s); the submit button shows a "Sending…" loading state.
|
|
20
|
-
- Closes on the × button, clicking the backdrop, or pressing `Esc`; locks background scroll while open and respects `prefers-reduced-motion`.
|
|
21
|
-
- Optional email/SMS marketing opt-ins, each shown only when its field is: "Email me store updates" with `email`, "Text me store updates" with `mobile`.
|
|
22
|
-
- Configurable from one JSON block, with the widget's main strings overridable — see [JSON configuration](#json-configuration) and [Copy](#copy).
|
|
23
|
-
- Optional product summary (image, name, size, now/was price) — see [Product summary](#product-summary).
|
|
24
|
-
- Theme integration through CSS custom properties and per-element theme classes — see [Styling and theme classes](#styling-and-theme-classes).
|
|
25
|
-
- Two display modes: a centred modal (default) or a right-hand slide-in panel — see [Display modes](#display-modes).
|
|
26
|
-
- Opens from any element marked `data-twc-notify-me` — PLP size buttons, "Size out of stock?" links — for the product it belongs to, with the right size pre-selected. See [Opening from product listing pages](#opening-from-product-listing-pages).
|
|
27
|
-
- Two auth modes: Shopify App Proxy (default) or a merchant-supplied token — see [Configuration](#configuration).
|
|
28
|
-
- Optional "Shop similar styles" section under the form, showing personalised product recommendations for the customer.
|
|
13
|
+
Three pieces go into the theme. This example is a Shopify product page.
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
**1. The product**, so the widget knows which sizes are out of stock:
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
npm run build # emit build/notify-me-wl.min.js
|
|
45
|
-
npm run dev # rebuild on change (watch mode)
|
|
46
|
-
npm run typecheck # type-check without emitting
|
|
47
|
-
npm run selftest # build, then run the harness pages in headless Chrome
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
> **v2.0.0 path change:** the distributed bundle moved from `index.js` to
|
|
51
|
-
> `build/notify-me-wl.min.js`. Update embeds to the new CDN URL above.
|
|
52
|
-
|
|
53
|
-
### Usage
|
|
54
|
-
|
|
55
|
-
Add `<div id="notification-widget"></div>` once per page and put the configuration attributes below on it (or, as older installs do, on a `<button id="popup-open">` — its attributes win where both carry one). Anything marked `data-twc-notify-me` opens the widget; the legacy `#popup-open` button does too. `data-fields` is a JSON string of the fields to include, and `data-type` is `notify-me` or `coming-soon`.
|
|
56
|
-
|
|
57
|
-
```html
|
|
58
|
-
<div id="notification-widget"></div>
|
|
59
|
-
<button id="popup-open" data-fields='["mobile", "firstName", "lastName"]' data-type="notify-me">Open Popup</button>
|
|
17
|
+
```liquid
|
|
18
|
+
<script>
|
|
19
|
+
window.currentProduct = {
|
|
20
|
+
id: {{ product.id | json }},
|
|
21
|
+
title: {{ product.title | json }},
|
|
22
|
+
variants: [
|
|
23
|
+
{%- for v in product.variants -%}
|
|
24
|
+
{ id: {{ v.id | json }}, title: {{ v.title | json }}, available: {{ v.available | json }} }{% unless forloop.last %},{% endunless %}
|
|
25
|
+
{%- endfor -%}
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
60
29
|
```
|
|
61
30
|
|
|
62
|
-
|
|
63
|
-
include Shopify's `available` flag — the widget then lists only out-of-stock sizes
|
|
64
|
-
itself — or be pre-filtered in Liquid as older installs do.
|
|
65
|
-
|
|
66
|
-
### Configuration
|
|
67
|
-
|
|
68
|
-
Every attribute below can be set on `#notification-widget`, on `#popup-open`, or as a key in the JSON block (see [JSON configuration](#json-configuration)). Per setting, `#popup-open` wins, then `#notification-widget`, then the JSON block.
|
|
69
|
-
|
|
70
|
-
- `data-fields`: A JSON array specifying the optional fields to include in the form. Possible values are `"email"`, `"mobile"`, `"firstName"`, `"lastName"`, `"countryCode"`, and `"provinceCode"`. Defaults to `["email"]`. (A required Size selector is always shown and populated from the product's variants.)
|
|
71
|
-
- `data-type`: Specifies the type of the widget. Possible values are `"notify-me"` and `"coming-soon"`.
|
|
72
|
-
- `data-display`: How the widget presents itself. `"modal"` (default) is a centred dialog; `"panel"` slides in from the right edge at full height. Any other value falls back to `"modal"`. See [Display modes](#display-modes).
|
|
73
|
-
- `data-auth`: Auth mode. `"proxy"` (default) uses the Shopify App Proxy at `/apps/<proxy-app>/auth/token` to obtain a tenant-scoped token (requires the customer to be logged in to the storefront). `"token"` sends a token you supply via `data-access-token` or `window.TWC_ACCESS_TOKEN`. **The widget bundles no credential of its own** — see [Auth modes](#auth-modes).
|
|
74
|
-
- `data-tenant`: The TWC tenant sent as the `X-Twc-Tenant` header. Used in both auth modes. Falls back to the bundled `TENANT_ID` if omitted.
|
|
75
|
-
- `data-proxy-app`: The Shopify App Proxy subpath, forming the `/apps/<proxy-app>/...` URL prefix used by the `"proxy"` auth mode. Falls back to the bundled `PROXY_APP_NAME` (`twc-sdk`) if omitted.
|
|
76
|
-
- `data-access-token`: Access token for the `"token"` auth mode, with or without the `Bearer ` prefix. Takes precedence over `window.TWC_ACCESS_TOKEN`. Ignored in `"proxy"` mode. Anything rendered here is visible to anyone viewing the page — use a short-lived, narrowly scoped token, never a staff or POS credential.
|
|
77
|
-
- `data-market-id`: The Shopify market the shopper is browsing in. Accepts either the numeric id or the full GID — `gid://shopify/Market/12345` and `12345` both send `12345`. Falls back to `window.Shopify.markets.currentMarket.id` when omitted. See [Shopify markets](#shopify-markets).
|
|
78
|
-
- `data-market-handle`: The market handle (e.g. `"au"`). Attribute only — there is no runtime fallback, so a theme that does not render it reports no handle.
|
|
79
|
-
- `data-country-code`: Two-letter ISO country code for the market the shopper is browsing in. Takes priority over the automatic detection chain, but a country the shopper picks in the form still wins over both.
|
|
80
|
-
- `data-recommendations`: Set to `"false"` to disable the "Shop similar styles" section. Enabled by default.
|
|
81
|
-
- `data-recommendations-count`: How many products to show. Defaults to `8`. Non-numeric or non-positive values fall back to `8`.
|
|
82
|
-
- `data-recommendations-profile`: Athos/Searchspring recommendation profile tag for this install (e.g. `"similar-snap"`). Only consulted when the tenant config carries no `profileTag`/`tags`; see [Choosing an engine](#choosing-an-engine). Ignored by the `TWC` engine.
|
|
83
|
-
- `data-currency`: Fallback ISO currency code (e.g. `"AUD"`) for formatting recommendation prices, used only when the storefront does not expose `window.Shopify.currency.active`. Without either, prices render as bare numbers rather than risking the wrong symbol.
|
|
84
|
-
- `data-customer-email`: Overrides the customer email used to request recommendations. Intended for testing — in production the email comes from the Shopify customer context, or from the email the shopper submits.
|
|
85
|
-
|
|
86
|
-
### JSON configuration
|
|
87
|
-
|
|
88
|
-
Instead of (or as well as) attributes, put a JSON block inside `#notification-widget`:
|
|
31
|
+
**2. The widget and its configuration**, once per page:
|
|
89
32
|
|
|
90
33
|
```liquid
|
|
91
|
-
<div id="notification-widget"
|
|
34
|
+
<div id="notification-widget"
|
|
35
|
+
data-market-id="{{ localization.market.id }}"
|
|
36
|
+
data-market-handle="{{ localization.market.handle }}"
|
|
37
|
+
data-country-code="{{ localization.country.iso_code }}">
|
|
92
38
|
<script type="application/json" data-twc-config>
|
|
93
39
|
{
|
|
94
40
|
"tenant": "your-tenant",
|
|
95
41
|
"type": "notify-me",
|
|
96
42
|
"display": "panel",
|
|
97
|
-
"fields": ["email"]
|
|
98
|
-
"currency": {{ cart.currency.iso_code | json }},
|
|
99
|
-
"productSummary": true,
|
|
100
|
-
"copy": { "title": {{ 'Notify me when available' | json }} },
|
|
101
|
-
"classes": { "submit": "Button Button--primary" }
|
|
43
|
+
"fields": ["email"]
|
|
102
44
|
}
|
|
103
45
|
</script>
|
|
104
46
|
</div>
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/notify-me-wl@3.6.2/build/notify-me-wl.min.js" defer></script>
|
|
105
48
|
```
|
|
106
49
|
|
|
107
|
-
|
|
108
|
-
- `productSummary`, `copy` and `classes` exist only in JSON.
|
|
109
|
-
- Per setting, an attribute on `#popup-open` wins, then one on `#notification-widget`, then the JSON block, then the default. Liquid-rendered values such as `data-market-id` can stay as attributes next to the block.
|
|
110
|
-
- Build values with Liquid's `| json` filter so quotes and apostrophes cannot break the block.
|
|
111
|
-
- `null` counts as not set. A value of the wrong type is ignored with a console warning and the next layer applies. Invalid JSON is reported in the console and the widget runs as if there were no block.
|
|
112
|
-
- The header row (`.twc-nm-header`, and so `--twc-nm-header-border`) is rendered only when a `data-twc-config` block is present.
|
|
113
|
-
- String settings accept numbers too, so `"marketId": {{ localization.market.id | json }}` works.
|
|
50
|
+
**3. A button that opens it:**
|
|
114
51
|
|
|
115
|
-
|
|
52
|
+
```html
|
|
53
|
+
<button type="button" data-twc-notify-me>Notify me</button>
|
|
54
|
+
```
|
|
116
55
|
|
|
117
|
-
|
|
56
|
+
That's a working install. Always pin the script to an exact version (`@3.6.2`), so a new release never reaches a live store until you choose to upgrade.
|
|
118
57
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
| `intro` | "Register to receive a notification as soon as this item is back in stock" (notify-me) / "Register your interest to hear more about this item" (coming-soon) |
|
|
123
|
-
| `sizeLabel` | `"Size"` |
|
|
124
|
-
| `sizePlaceholder` | `"Select a size"` |
|
|
125
|
-
| `emailLabel` | `"Email"`; `false` removes the label (the input gets an `aria-label`) |
|
|
126
|
-
| `emailPlaceholder` | `"you@example.com"` |
|
|
127
|
-
| `emailOptIn` | `"Email me store updates"` (shown with the `email` field) |
|
|
128
|
-
| `smsOptIn` | `"Text me store updates"` (shown with the `mobile` field) |
|
|
129
|
-
| `submit` | `"Notify Me"` / `"Register Interest"` |
|
|
130
|
-
| `success` | `"You're on the list. We'll be in touch."` |
|
|
131
|
-
| `disclaimer` | none; when set, shown under the submit button |
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Product data
|
|
132
61
|
|
|
133
|
-
|
|
62
|
+
The widget reads the product from the clicked element's closest `[data-twc-product]`, or from `window.currentProduct` on a product page.
|
|
134
63
|
|
|
135
|
-
|
|
64
|
+
| Field | Required | Meaning |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `id`, `title` | yes | Product id and name |
|
|
67
|
+
| `variants[].id`, `variants[].title` | yes | Each size. `title` is what the size dropdown shows |
|
|
68
|
+
| `variants[].available` | recommended | Only sizes that are not `true` are offered. Without it, every listed variant is offered (older installs pre-filter in Liquid) |
|
|
69
|
+
| `image` | for the summary | Product image URL, used when a variant has none |
|
|
70
|
+
| `variants[].price` | for the summary | Already-formatted price, e.g. `"$199.96 AUD"` |
|
|
71
|
+
| `variants[].compareAtPrice` | for the summary | Already-formatted "was" price. Only send it when it is a real discount |
|
|
72
|
+
| `variants[].image` | for the summary | Variant image URL |
|
|
136
73
|
|
|
137
|
-
`"productSummary": true`
|
|
74
|
+
The last four only matter with `"productSummary": true` (see [Configuration](#configuration)). Prices are passed pre-formatted so they always match the theme's own price display. Image URLs must start with `https:`, `http:` or `//`.
|
|
138
75
|
|
|
139
|
-
|
|
76
|
+
A full capture, including summary fields, that works for both product pages and cards:
|
|
140
77
|
|
|
141
78
|
```liquid
|
|
142
79
|
{%- capture twc_product -%}
|
|
143
|
-
{"id":{{ product.id | json }},"title":{{ product.title | json }}
|
|
80
|
+
{"id":{{ product.id | json }},"title":{{ product.title | json }}
|
|
144
81
|
{%- if product.featured_image -%},"image":{{ product.featured_image | image_url: width: 300 | json }}{%- endif -%},"variants":[
|
|
145
82
|
{%- for v in product.variants -%}
|
|
146
83
|
{"id":{{ v.id | json }},"title":{{ v.title | json }},"available":{{ v.available | json }},
|
|
@@ -150,374 +87,253 @@ It reads optional fields on the product data (`window.currentProduct` or `data-t
|
|
|
150
87
|
{%- unless forloop.last -%},{%- endunless -%}
|
|
151
88
|
{%- endfor -%}]}
|
|
152
89
|
{%- endcapture -%}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Emit it on a product page as `<script>window.currentProduct = {{ twc_product }};</script>`, or on a product card as `<div data-twc-product="{{ twc_product | escape }}">` (`| escape` is required inside the attribute).
|
|
156
|
-
|
|
157
|
-
A variant without an image falls back to the product image. Image URLs must be `https:`, `http:` or `//` URLs. With no size selected, the price shows only when every listed size has the same price.
|
|
158
|
-
|
|
159
|
-
### Styling and theme classes
|
|
160
90
|
|
|
161
|
-
|
|
91
|
+
{%- comment -%} Product page {%- endcomment -%}
|
|
92
|
+
<script>window.currentProduct = {{ twc_product }};</script>
|
|
162
93
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
| `--twc-nm-panel-width` | `420px` |
|
|
166
|
-
| `--twc-nm-panel-width-mobile` | `100%` (≤480px) |
|
|
167
|
-
| `--twc-nm-padding` | `24px` |
|
|
168
|
-
| `--twc-nm-overlay-bg` | `rgba(17, 17, 17, 0.55)` |
|
|
169
|
-
| `--twc-nm-panel-shadow` | `-10px 0 40px rgba(0, 0, 0, 0.18)` |
|
|
170
|
-
| `--twc-nm-transition` | `200ms ease` |
|
|
171
|
-
| `--twc-nm-bg` | `#fff` |
|
|
172
|
-
| `--twc-nm-text` | `#111827` |
|
|
173
|
-
| `--twc-nm-muted` | `#6b7280` |
|
|
174
|
-
| `--twc-nm-border` | `#e5e7eb` |
|
|
175
|
-
| `--twc-nm-font` | `inherit` |
|
|
176
|
-
| `--twc-nm-radius` | `8px` |
|
|
177
|
-
| `--twc-nm-header-border` | `none` |
|
|
178
|
-
|
|
179
|
-
**Theme classes.** `classes` in the JSON block adds your theme's classes to the widget's elements, so they pick up your existing button, input and heading styles:
|
|
180
|
-
|
|
181
|
-
```json
|
|
182
|
-
"classes": {
|
|
183
|
-
"title": "Heading u-h4",
|
|
184
|
-
"input": "Form__Input",
|
|
185
|
-
"select": "Form__Input",
|
|
186
|
-
"submit": "Button Button--primary Button--full"
|
|
187
|
-
}
|
|
94
|
+
{%- comment -%} Product card — | escape is required inside an attribute {%- endcomment -%}
|
|
95
|
+
<div class="product-card" data-twc-product="{{ twc_product | escape }}">…</div>
|
|
188
96
|
```
|
|
189
97
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
An element given classes drops the widget's own colours, fonts, borders, padding and hover styles, so your theme's apply cleanly. Its layout (width, position, truncation) stays the widget's. The close button keeps its border, background and box-shadow reset and its positioning, and a themed select keeps the widget's arrow image (give it enough right padding, about 36px). Class names may contain letters, digits, `-` and `_`; anything else is dropped with a console warning.
|
|
193
|
-
|
|
194
|
-
### Auth modes
|
|
98
|
+
Keep the `{%- if … -%}` guards: `image_url` on a missing image renders a Liquid error and breaks the JSON.
|
|
195
99
|
|
|
196
|
-
|
|
197
|
-
`data-auth` chooses where the token comes from. **The distributed bundle contains no
|
|
198
|
-
access token** — a `"token"` install that supplies none will fail to submit.
|
|
100
|
+
---
|
|
199
101
|
|
|
200
|
-
|
|
102
|
+
## Opening the widget
|
|
201
103
|
|
|
202
|
-
|
|
203
|
-
endpoint `/apps/<proxy-app>/auth/token` on form submit (where `<proxy-app>` comes from
|
|
204
|
-
`data-proxy-app`, defaulting to `twc-sdk`; Shopify signs and forwards the request). The
|
|
205
|
-
token is cached for the page session and reused across submissions.
|
|
104
|
+
Any element with `data-twc-notify-me` opens the widget for its product. No theme JavaScript is needed, and cards added later (filters, infinite scroll) work too.
|
|
206
105
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
106
|
+
```liquid
|
|
107
|
+
<div class="product-card" data-twc-product="{{ twc_product | escape }}">
|
|
108
|
+
{%- for v in product.variants -%}
|
|
109
|
+
<button type="button" data-variant-id="{{ v.id }}" {% unless v.available %}data-twc-notify-me{% endunless %}>{{ v.title }}</button>
|
|
110
|
+
{%- endfor -%}
|
|
111
|
+
<a href="#" data-twc-notify-me>Size out of stock?</a>
|
|
112
|
+
</div>
|
|
113
|
+
```
|
|
210
114
|
|
|
211
|
-
|
|
115
|
+
- Don't put `disabled` on a sold-out size button. Browsers don't fire clicks on disabled buttons, so use a class or `aria-disabled="true"` instead.
|
|
116
|
+
- The click's default action is prevented (`href="#"` won't jump), but the theme's own handlers still run.
|
|
212
117
|
|
|
213
|
-
|
|
214
|
-
`window.TWC_ACCESS_TOKEN`. The attribute wins if both are present, and the `Bearer `
|
|
215
|
-
prefix is added if you omit it.
|
|
118
|
+
**Which size is pre-selected.** The first of these that matches an out-of-stock size wins:
|
|
216
119
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
120
|
+
1. `data-variant-id` on the clicked element.
|
|
121
|
+
2. On a product page only: `?variant=` in the URL, then the product form's `select[name="id"]` (inside `form[action*="/cart/add"]`).
|
|
122
|
+
3. The only out-of-stock size, if there is exactly one.
|
|
123
|
+
4. Otherwise "Select a size".
|
|
221
124
|
|
|
222
|
-
|
|
223
|
-
page or the bundle. Mint a short-lived token scoped to customer-interest writes for the
|
|
224
|
-
single tenant — never a staff, POS, or store-owner credential. If you cannot mint one
|
|
225
|
-
that narrow, use `"proxy"` instead.
|
|
125
|
+
A product with nothing out of stock shows "All sizes are currently in stock." instead of the form.
|
|
226
126
|
|
|
227
|
-
|
|
228
|
-
submission fails with the generic error message.
|
|
127
|
+
---
|
|
229
128
|
|
|
230
|
-
|
|
231
|
-
> `"proxy"`, and the previously bundled access token was removed. Installs that relied
|
|
232
|
-
> on the bundled token must either set up the App Proxy or supply their own token via
|
|
233
|
-
> `data-access-token`.
|
|
129
|
+
## Configuration
|
|
234
130
|
|
|
235
|
-
|
|
131
|
+
Put settings in the JSON block inside `#notification-widget`. Every setting except `productSummary`, `copy` and `classes` can also be an attribute: the attribute name is `data-` plus the key in kebab-case (`proxyApp` → `data-proxy-app`). Attributes are handy for values Liquid renders, like the market id.
|
|
236
132
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
133
|
+
| Key | Attribute | Default | Description |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| `tenant` | `data-tenant` | bundled demo tenant | Your TWC tenant. **Always set it** |
|
|
136
|
+
| `type` | `data-type` | `"notify-me"` | `"notify-me"` or `"coming-soon"`. Changes the default copy and the payload flag |
|
|
137
|
+
| `display` | `data-display` | `"modal"` | `"modal"` (centred dialog) or `"panel"` (full-height, slides in from the right) |
|
|
138
|
+
| `fields` | `data-fields` | `["email"]` | Form fields, any of `"email"`, `"mobile"`, `"firstName"`, `"lastName"`, `"countryCode"`, `"provinceCode"`. The size dropdown is always shown |
|
|
139
|
+
| `auth` | `data-auth` | `"proxy"` | `"proxy"` or `"token"`; see [Authentication](#authentication) |
|
|
140
|
+
| `proxyApp` | `data-proxy-app` | `"twc-sdk"` | Shopify App Proxy subpath (`/apps/<proxyApp>/…`) |
|
|
141
|
+
| `accessToken` | `data-access-token` | — | Token for `"token"` auth. Falls back to `window.TWC_ACCESS_TOKEN` |
|
|
142
|
+
| `marketId` | `data-market-id` | `window.Shopify` market | Shopify market id; a full `gid://…` is fine |
|
|
143
|
+
| `marketHandle` | `data-market-handle` | — | Market handle, e.g. `"au"` |
|
|
144
|
+
| `countryCode` | `data-country-code` | detected | ISO country code. A country chosen in the form wins |
|
|
145
|
+
| `currency` | `data-currency` | `window.Shopify.currency.active` | Fallback currency for recommendation prices |
|
|
146
|
+
| `customerEmail` | `data-customer-email` | Shopify customer | Overrides the shopper's email (testing) |
|
|
147
|
+
| `recommendations` | `data-recommendations` | `true` | `false` hides the "Shop similar styles" row |
|
|
148
|
+
| `recommendationsCount` | `data-recommendations-count` | `8` | Products in that row |
|
|
149
|
+
| `recommendationsProfile` | `data-recommendations-profile` | — | Searchspring profile tag, if the tenant config has none |
|
|
150
|
+
| `productSummary` | — | `false` | Show image, name, size and price at the top of the form |
|
|
151
|
+
| `copy` | — | — | Text overrides; see [Copy](#copy) |
|
|
152
|
+
| `classes` | — | — | Theme classes per element; see [Styling](#styling) |
|
|
240
153
|
|
|
241
|
-
|
|
242
|
-
| --- | --- | --- |
|
|
243
|
-
| Position | Centred | Docked to the right edge |
|
|
244
|
-
| Size | 400px wide, fits the viewport | 420px wide, full height |
|
|
245
|
-
| Corners | Rounded | Square |
|
|
246
|
-
| Entry | Fades and lifts | Slides in from the right |
|
|
247
|
-
| Below 480px | Unchanged | Spans the full viewport width |
|
|
154
|
+
**Rules**
|
|
248
155
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
156
|
+
- **Precedence:** an attribute on the legacy `#popup-open` button wins, then an attribute on `#notification-widget`, then the JSON block, then the default. An empty attribute counts as not set.
|
|
157
|
+
- **Use Liquid's `| json` filter** for any value you render into the block, so quotes and apostrophes can't break it.
|
|
158
|
+
- **Bad values are skipped, not fatal.** A value of the wrong type is ignored with a console warning, and `null` counts as not set. Invalid JSON is logged and the widget runs with the remaining settings.
|
|
159
|
+
- **One `#notification-widget` per page.** If there are two, the first is used.
|
|
253
160
|
|
|
254
|
-
|
|
255
|
-
changes nothing about the widget's behaviour, so anything documented elsewhere in this
|
|
256
|
-
README applies to both modes. Under `prefers-reduced-motion` the panel appears without
|
|
257
|
-
sliding.
|
|
161
|
+
---
|
|
258
162
|
|
|
259
|
-
|
|
163
|
+
## Copy
|
|
260
164
|
|
|
261
|
-
|
|
262
|
-
market. The values come from Liquid, which renders server-side, so the theme passes
|
|
263
|
-
them to the widget as attributes on `#notification-widget` (or the legacy `#popup-open` button):
|
|
165
|
+
`copy` overrides the widget's main text. Everything is shown as plain text, never HTML.
|
|
264
166
|
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
167
|
+
```json
|
|
168
|
+
"copy": {
|
|
169
|
+
"title": "Notify me when available",
|
|
170
|
+
"intro": "We'll email you as soon as {product} is back.",
|
|
171
|
+
"emailLabel": false,
|
|
172
|
+
"emailPlaceholder": "Email",
|
|
173
|
+
"submit": "NOTIFY ME",
|
|
174
|
+
"disclaimer": "You can unsubscribe at any time."
|
|
175
|
+
}
|
|
273
176
|
```
|
|
274
177
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
178
|
+
| Key | Default |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `title` | `"{product}"` (the product name) |
|
|
181
|
+
| `intro` | "Register to receive a notification as soon as this item is back in stock" / "Register your interest to hear more about this item" |
|
|
182
|
+
| `sizeLabel` / `sizePlaceholder` | `"Size"` / `"Select a size"` |
|
|
183
|
+
| `emailLabel` | `"Email"`. `false` hides the label; the input keeps an accessible name |
|
|
184
|
+
| `emailPlaceholder` | `"you@example.com"` |
|
|
185
|
+
| `emailOptIn` | `"Email me store updates"` (shown when `fields` includes `email`) |
|
|
186
|
+
| `smsOptIn` | `"Text me store updates"` (shown when `fields` includes `mobile`) |
|
|
187
|
+
| `submit` | `"Notify Me"` / `"Register Interest"` |
|
|
188
|
+
| `success` | `"You're on the list. We'll be in touch."` |
|
|
189
|
+
| `disclaimer` | none. When set, it is shown under the submit button |
|
|
278
190
|
|
|
279
|
-
|
|
280
|
-
configured, or an unrendered Liquid tag — is dropped, and the field is left out of the
|
|
281
|
-
payload rather than sent as `null`. All three attributes are optional: without them,
|
|
282
|
-
`marketId` and `countryCode` still resolve from `window.Shopify` and the browser locale,
|
|
283
|
-
and `marketHandle` is simply not reported.
|
|
191
|
+
`{product}` in `title` or `intro` becomes the product name. Not yet configurable: the status messages ("Sending…", "Please select a size.", errors), the close button label, and the mobile, name, country and province field labels.
|
|
284
192
|
|
|
285
|
-
|
|
286
|
-
takes precedence over `data-country-code`. The dropdown is pre-selected to the market's
|
|
287
|
-
country, so leaving it untouched sends the market country either way.
|
|
193
|
+
---
|
|
288
194
|
|
|
289
|
-
|
|
195
|
+
## Styling
|
|
290
196
|
|
|
291
|
-
|
|
197
|
+
All widget styles are scoped under `#twc-nm-overlay`, and the widget uses the page's font. There are two ways to match a theme. They can be combined.
|
|
292
198
|
|
|
293
|
-
|
|
294
|
-
| --- | --- | --- |
|
|
295
|
-
| `data-twc-product` | A product container, e.g. the card | JSON `{ id, title, variants: [{ id, title, available }] }` |
|
|
296
|
-
| `data-twc-notify-me` | Any clickable element | Clicking it opens the widget |
|
|
297
|
-
| `data-variant-id` | Optional, on the same element | Variant to pre-select |
|
|
199
|
+
### CSS variables
|
|
298
200
|
|
|
299
|
-
|
|
300
|
-
falling back to `window.currentProduct`. It listens at page level in the capture phase,
|
|
301
|
-
so cards added later (filters, infinite scroll) work, and a theme handler that stops
|
|
302
|
-
propagation does not block it. It calls `preventDefault()` on the click — `href="#"`
|
|
303
|
-
does not jump — but lets the theme's own handlers run.
|
|
201
|
+
Set any of these on `:root` (or any ancestor, inside media queries if needed). Anything you don't set keeps the default look.
|
|
304
202
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
203
|
+
| Variable | Default |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `--twc-nm-panel-width` | `420px` |
|
|
206
|
+
| `--twc-nm-panel-width-mobile` | `100%` (at ≤480px) |
|
|
207
|
+
| `--twc-nm-padding` | `24px` |
|
|
208
|
+
| `--twc-nm-overlay-bg` | `rgba(17, 17, 17, 0.55)` |
|
|
209
|
+
| `--twc-nm-panel-shadow` | `-10px 0 40px rgba(0, 0, 0, 0.18)` |
|
|
210
|
+
| `--twc-nm-transition` | `200ms ease` |
|
|
211
|
+
| `--twc-nm-bg` / `--twc-nm-text` / `--twc-nm-muted` / `--twc-nm-border` | `#fff` / `#111827` / `#6b7280` / `#e5e7eb` |
|
|
212
|
+
| `--twc-nm-font` | `inherit` |
|
|
213
|
+
| `--twc-nm-radius` | `8px` |
|
|
214
|
+
| `--twc-nm-header-border` | `none` |
|
|
308
215
|
|
|
309
|
-
```
|
|
310
|
-
{
|
|
311
|
-
{"id":{{ product.id | json }},"title":{{ product.title | json }},"variants":[
|
|
312
|
-
{%- for v in product.variants -%}
|
|
313
|
-
{"id":{{ v.id | json }},"title":{{ v.title | json }},"available":{{ v.available | json }}}
|
|
314
|
-
{%- unless forloop.last -%},{%- endunless -%}
|
|
315
|
-
{%- endfor -%}]}
|
|
316
|
-
{%- endcapture -%}
|
|
317
|
-
<div class="product-card" data-twc-product="{{ twc_product | escape }}">
|
|
318
|
-
{%- for variant in product.variants -%}
|
|
319
|
-
<button type="button" data-variant-id="{{ variant.id }}"
|
|
320
|
-
{% unless variant.available %}data-twc-notify-me{% endunless %}>{{ variant.title }}</button>
|
|
321
|
-
{%- endfor -%}
|
|
322
|
-
<a href="#" data-twc-notify-me>Size out of stock?</a>
|
|
323
|
-
</div>
|
|
216
|
+
```css
|
|
217
|
+
:root { --twc-nm-panel-width: 400px; --twc-nm-radius: 0; --twc-nm-overlay-bg: rgba(54, 54, 54, 0.5); }
|
|
324
218
|
```
|
|
325
219
|
|
|
326
|
-
|
|
327
|
-
dispatch clicks on disabled buttons. Style them with a class or `aria-disabled="true"`
|
|
328
|
-
instead.
|
|
220
|
+
### Theme classes
|
|
329
221
|
|
|
330
|
-
|
|
331
|
-
`theme.liquid`), and load the script with `defer` or no attribute:
|
|
222
|
+
`classes` adds your theme's own classes to the widget's elements, so buttons, inputs and headings look exactly like the rest of the store:
|
|
332
223
|
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
data-market-handle="{{ localization.market.handle }}"
|
|
341
|
-
data-country-code="{{ localization.country.iso_code }}"></div>
|
|
342
|
-
<script src="https://cdn.jsdelivr.net/npm/notify-me-wl@3.5.0/build/notify-me-wl.min.js" defer></script>
|
|
224
|
+
```json
|
|
225
|
+
"classes": {
|
|
226
|
+
"title": "Heading u-h4",
|
|
227
|
+
"input": "Form__Input",
|
|
228
|
+
"select": "Form__Input",
|
|
229
|
+
"submit": "Button Button--primary Button--full"
|
|
230
|
+
}
|
|
343
231
|
```
|
|
344
232
|
|
|
345
|
-
|
|
346
|
-
|
|
233
|
+
**Keys:**
|
|
234
|
+
- Form elements: `title`, `intro`, `label`, `input`, `select`, `checkbox`, `submit`, `close`, `disclaimer`.
|
|
235
|
+
- Product summary: `summary`, `summaryImage`, `summaryName`, `summaryVariant`, `summaryPrice`, `summaryCompareAt`.
|
|
236
|
+
- `label`, `input` and `select` apply to every element of that kind.
|
|
347
237
|
|
|
348
|
-
|
|
238
|
+
**How styling is split:**
|
|
239
|
+
- An element with classes drops the widget's own colours, fonts, borders, padding and hover styles, so the theme's apply cleanly.
|
|
240
|
+
- Layout (width, positioning, text truncation) always stays the widget's.
|
|
241
|
+
- A themed select keeps the widget's dropdown arrow, so give it about 36px of right padding.
|
|
242
|
+
- Class names may only contain letters, digits, `-` and `_`. Anything else is dropped, with a warning in the console.
|
|
349
243
|
|
|
350
|
-
|
|
351
|
-
them wins:
|
|
244
|
+
**Header row.** When the JSON block is present, the title and close button sit in a `.twc-nm-header` row you can style. This is also what `--twc-nm-header-border` applies to. Without a JSON block, the markup is exactly as in earlier versions.
|
|
352
245
|
|
|
353
|
-
|
|
354
|
-
2. Product page only (the product came from `window.currentProduct`): `?variant=` in
|
|
355
|
-
the URL, then the value of the first `select[name="id"]` inside a
|
|
356
|
-
`form[action*="/cart/add"]`. Hidden `input[name="id"]` fields are ignored — some
|
|
357
|
-
themes keep a stale one in an installments form. Never applied to a card, so page
|
|
358
|
-
state cannot leak into a listing.
|
|
359
|
-
3. Exactly one out-of-stock size — that size.
|
|
360
|
-
4. Otherwise, "Select a size".
|
|
246
|
+
**Overriding widget rules.** The widget injects its stylesheet when it loads, after your theme's CSS. A theme rule must therefore be more specific than the widget's to win, e.g. `#twc-nm-overlay .twc-nm-card .twc-nm-title`.
|
|
361
247
|
|
|
362
|
-
|
|
363
|
-
of the form. A missing or malformed product shows "Product information is unavailable.
|
|
364
|
-
Please refresh the page."
|
|
248
|
+
---
|
|
365
249
|
|
|
366
|
-
|
|
250
|
+
## Authentication
|
|
367
251
|
|
|
368
|
-
|
|
369
|
-
beneath the form, fetched from:
|
|
252
|
+
Every request carries `Authorization: Bearer <token>` and `X-Twc-Tenant: <tenant>`. The bundle ships no credential.
|
|
370
253
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
```
|
|
254
|
+
- **`"proxy"` (default):** on submit, the widget fetches a tenant-scoped token from the store's Shopify App Proxy at `/apps/<proxyApp>/auth/token` and reuses it for the page session. If the proxy returns no token, the widget shows "Please log in to your account to continue." and keeps the form open.
|
|
255
|
+
- **`"token"`:** the widget sends the token you supply in `accessToken` / `data-access-token` or `window.TWC_ACCESS_TOKEN`. Anything rendered into the page is public, so only use a short-lived token scoped to customer-interest writes for one tenant. Never use a staff or POS credential. If you can't mint one that narrow, use `"proxy"`.
|
|
374
256
|
|
|
375
|
-
|
|
376
|
-
customer-interest call, and the tenant doubles as the retailer id in the path.
|
|
257
|
+
---
|
|
377
258
|
|
|
378
|
-
|
|
259
|
+
## Recommendations
|
|
379
260
|
|
|
380
|
-
|
|
381
|
-
2. `window.customer.email` — a logged-in Shopify customer. The section then appears
|
|
382
|
-
as soon as the modal opens.
|
|
383
|
-
3. `window.customerEmail` — the same address exposed as a bare global, which is how
|
|
384
|
-
some themes already publish it. Also appears as soon as the modal opens.
|
|
385
|
-
4. The email the shopper submits through the form. The section appears after a
|
|
386
|
-
successful submit, and the modal stays open instead of auto-closing so they can
|
|
387
|
-
browse.
|
|
261
|
+
A "Shop similar styles" row can appear under the form. It shows only when **all** of these hold:
|
|
388
262
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
263
|
+
- `recommendations` is not `false`.
|
|
264
|
+
- The tenant's config defines an engine (`websiteRecommendations`, see below).
|
|
265
|
+
- A shopper email is known. It comes from the first of these that is set:
|
|
266
|
+
- `customerEmail`.
|
|
267
|
+
- `window.customer.email`.
|
|
268
|
+
- `window.customerEmail`.
|
|
269
|
+
- The email just submitted. In this case the row appears after submit, and the widget stays open instead of auto-closing.
|
|
270
|
+
|
|
271
|
+
A known email also pre-fills the form's email field. Shopify doesn't expose the customer's email by default; add it in the theme:
|
|
392
272
|
|
|
393
273
|
```liquid
|
|
394
|
-
<script>
|
|
395
|
-
{% if customer %}
|
|
396
|
-
window.customer = { email: {{ customer.email | json }} };
|
|
397
|
-
{% endif %}
|
|
398
|
-
</script>
|
|
274
|
+
<script>{% if customer %}window.customer = { email: {{ customer.email | json }} };{% endif %}</script>
|
|
399
275
|
```
|
|
400
276
|
|
|
401
|
-
|
|
402
|
-
it boolean `false` for guests. That is handled: the value is accepted only when it is a
|
|
403
|
-
non-empty string, so `false` resolves to "no email" rather than the string `"false"`.
|
|
404
|
-
|
|
405
|
-
When an email resolves from any of the first three sources, the form's Email field is
|
|
406
|
-
**prefilled** with it. The field stays editable, and submit uses whatever it contains at
|
|
407
|
-
that moment, so a shopper can send the notification to a different address.
|
|
408
|
-
|
|
409
|
-
#### Choosing an engine
|
|
410
|
-
|
|
411
|
-
Which engine powers the section is a per-tenant server-side setting, not a data
|
|
412
|
-
attribute. On first open the widget fetches the tenant config from:
|
|
277
|
+
The row never blocks or changes the notify-me submission. If anything fails, it simply doesn't appear.
|
|
413
278
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
```
|
|
279
|
+
<details>
|
|
280
|
+
<summary>Engine selection and internals</summary>
|
|
417
281
|
|
|
418
|
-
|
|
419
|
-
|
|
282
|
+
- **Engine:** read once per page from `GET /services/eventcollector/api/v1/custom/configs/public` (authenticated like every other call) → `websiteRecommendations`:
|
|
283
|
+
- `{ "engine": "TWC" }`: TWC's recommendations API, `/services/recommendations/api/v1/recommendations/<tenant>/<email>`.
|
|
284
|
+
- `{ "engine": "ATHOS", "siteIdentifier": "abc123" }`: Searchspring, `https://abc123.a.searchspring.io/boost/abc123/recommend`, called with **no** TWC credentials.
|
|
285
|
+
- When the field is missing, the engine is unknown, or ATHOS has no `siteIdentifier`, no row is shown. A failed config request is retried on the next open.
|
|
286
|
+
- **Searchspring profile tag:** `profileTag`, then `tags` from the tenant config, then `recommendationsProfile`, then `"similar"`. The tag must exist on the Searchspring site; an unknown tag returns no results. Check with:
|
|
287
|
+
`curl -s "https://{site}.a.searchspring.io/boost/{site}/recommend?tags={tag}&limits=3&products={productId}"`
|
|
288
|
+
- **TWC results** come per variant, so the widget over-fetches (up to the API's limit of `n=20`) and dedupes by product.
|
|
289
|
+
- **Prices** use `window.Shopify.currency.active`, then `currency`; with neither they render as bare numbers.
|
|
420
290
|
|
|
421
|
-
|
|
422
|
-
"websiteRecommendations": { "engine": "TWC" }
|
|
423
|
-
```
|
|
291
|
+
</details>
|
|
424
292
|
|
|
425
|
-
|
|
426
|
-
"websiteRecommendations": { "engine": "ATHOS", "siteIdentifier": "abc123" }
|
|
427
|
-
```
|
|
293
|
+
---
|
|
428
294
|
|
|
429
|
-
|
|
430
|
-
request is made and no placeholders appear. The older top-level `recommendationsEngine`
|
|
431
|
-
field is ignored. The section is likewise skipped when the engine is unrecognised, or
|
|
432
|
-
when `ATHOS` has no `siteIdentifier`. The config is fetched once per page session, and a
|
|
433
|
-
failed config request is not cached, so a transient outage does not disable the section
|
|
434
|
-
for the rest of the visit.
|
|
295
|
+
## What gets sent
|
|
435
296
|
|
|
436
|
-
|
|
437
|
-
subdomain and the path:
|
|
297
|
+
`POST https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest`
|
|
438
298
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
299
|
+
| Field | When |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `variantRef` | Always: the size selected in the widget |
|
|
302
|
+
| `email` | Always (empty if the form has no email field) |
|
|
303
|
+
| `subscribe`, `subscribeSms` | Always: the opt-in checkboxes (`false` when not shown) |
|
|
304
|
+
| `notifyMe: true` or `comingSoon: true` | Depending on `type` |
|
|
305
|
+
| `mobile`, `firstName`, `lastName` | When those fields are in the form |
|
|
306
|
+
| `countryCode`, `provinceCode` | From the form if shown, otherwise detected |
|
|
307
|
+
| `marketId`, `marketHandle` | When known. `marketId` is always the bare number, never a `gid://` |
|
|
442
308
|
|
|
443
|
-
|
|
309
|
+
Marketing opt-ins are separate from the notify-me registration: shoppers can register without subscribing.
|
|
444
310
|
|
|
445
|
-
|
|
446
|
-
2. `tags` on `websiteRecommendations`.
|
|
447
|
-
3. `data-recommendations-profile` on `#notification-widget` (or `#popup-open`).
|
|
448
|
-
4. The bundled default, `similar`.
|
|
311
|
+
---
|
|
449
312
|
|
|
450
|
-
|
|
451
|
-
value, so a tenant that carries it must win. The attribute exists for merchants whose
|
|
452
|
-
config schema cannot express `profileTag` yet, letting a theme supply the tag without a
|
|
453
|
-
widget release.
|
|
313
|
+
## Development
|
|
454
314
|
|
|
455
|
-
|
|
456
|
-
returns `{"profile":{"type":"NotFound"},"results":[]}` with HTTP 200, which the widget
|
|
457
|
-
treats as "nothing usable" and renders no section. Verify with:
|
|
315
|
+
TypeScript in `src/`, bundled by Rollup into `build/notify-me-wl.min.js`. The bundle is committed and is what jsDelivr serves from npm.
|
|
458
316
|
|
|
459
317
|
```bash
|
|
460
|
-
|
|
318
|
+
npm install
|
|
319
|
+
npm run build # build/notify-me-wl.min.js
|
|
320
|
+
npm run dev # rebuild on change
|
|
321
|
+
npm run typecheck
|
|
322
|
+
npm run selftest # build, then run harness/*.html cases in headless Chrome
|
|
323
|
+
npm run visual # build, then pixel-compare default rendering against main
|
|
461
324
|
```
|
|
462
325
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
326
|
+
- **`selftest`** runs each page listed in `harness/run-selftest.sh`. Each page defines `window.SELFTEST_CASES`, and `harness/selftest.js` stubs every network call. Add a page to the list when you add one.
|
|
327
|
+
- **`visual`** screenshots the open widget (modal and panel, desktop and mobile, with and without hostile host CSS) using `main`'s bundle and yours, and fails on any pixel difference. It protects every existing install from unintended visual change. Use `BASELINE_REF=<ref>` to compare against another ref.
|
|
328
|
+
- Both scripts need Chrome. Set `CHROME=/path/to/chrome` off macOS.
|
|
329
|
+
- `index.html` is a manual demo page. Add `?mockSubmit=1` to log the payload instead of sending it.
|
|
466
330
|
|
|
467
|
-
|
|
468
|
-
never receive a tenant-scoped token.
|
|
331
|
+
---
|
|
469
332
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
The TWC service returns one entry per product variant, so several entries can describe
|
|
473
|
-
the same product in different colourways. The widget requests three times the display
|
|
474
|
-
count and dedupes by `product_ref`, keeping the highest-scoring entry, so a row never
|
|
475
|
-
repeats a product. **That API rejects `n` above 20 with a 422**, so the over-fetch is
|
|
476
|
-
clamped to 20 — at the default count of 8 that means `n=20`. Athos returns products
|
|
477
|
-
rather than variants, so it is asked for exactly `limits={count}` with no over-fetch.
|
|
478
|
-
|
|
479
|
-
The row scrolls horizontally. Touch devices pan it natively; on desktop it can also be
|
|
480
|
-
dragged with the mouse (click and drag, or use the scrollbar). A drag of more than a few
|
|
481
|
-
pixels suppresses the click that would otherwise open the product it started on.
|
|
482
|
-
|
|
483
|
-
Placeholder cards are shown while the request is in flight, so the section reserves its
|
|
484
|
-
space instead of the modal jumping when the products land. If the request returns
|
|
485
|
-
nothing usable, the placeholders are removed and the section disappears.
|
|
486
|
-
|
|
487
|
-
Prices use `window.Shopify.currency.active` when the storefront exposes it, falling back
|
|
488
|
-
to `data-currency`. With neither, prices render as bare numbers — showing `A$` on a GBP
|
|
489
|
-
price would be worse than showing no symbol at all.
|
|
490
|
-
|
|
491
|
-
If no email can be resolved, or the request fails or returns nothing, the section is
|
|
492
|
-
simply not rendered. It never blocks or alters the notify-me submission.
|
|
493
|
-
|
|
494
|
-
### Example
|
|
495
|
-
|
|
496
|
-
```html
|
|
497
|
-
<div id="notification-widget"></div>
|
|
498
|
-
<button id="popup-open" data-fields='["mobile"]' data-type="notify-me">Open Popup</button>
|
|
499
|
-
|
|
500
|
-
<script>
|
|
501
|
-
// The script provided will be included here
|
|
502
|
-
</script>
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### How It Works
|
|
506
|
-
|
|
507
|
-
1. **Initialization**: The script initializes on `DOMContentLoaded` (or immediately, if the page has already loaded), reading configuration from `#notification-widget` and `#popup-open`.
|
|
508
|
-
2. **Styles Injection**: It injects the modal/form styles, all namespaced under `#twc-nm-overlay` so they don't collide with the host theme.
|
|
509
|
-
3. **Modal Creation**: Creates the modal structure and appends it to the `notification-widget` div.
|
|
510
|
-
4. **Dynamic Content**: Based on the `data-type`, it sets the appropriate copy and button text, and builds the fields listed in `data-fields` plus the always-present Size selector.
|
|
511
|
-
5. **Opening**: A click on any `data-twc-notify-me` element (or `#popup-open`) resolves the product, rebuilds the Size selector with that product's out-of-stock sizes, and pre-selects one per [Size pre-selection](#size-pre-selection).
|
|
512
|
-
6. **Form Submission**: On submit, the button enters a "Sending…" state, the configured auth token is resolved, and the form data is POSTed to the API. The result is shown inline (success message then auto-close, or an error message); the popup is never closed on failure.
|
|
513
|
-
|
|
514
|
-
### API Request Example
|
|
515
|
-
|
|
516
|
-
```
|
|
517
|
-
https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest
|
|
518
|
-
```
|
|
333
|
+
## Upgrading
|
|
519
334
|
|
|
520
|
-
|
|
521
|
-
`
|
|
522
|
-
|
|
523
|
-
|
|
335
|
+
- **3.6.2:** on touch devices, opening the widget focuses the dialog rather than the size select, which iOS Safari otherwise would not open on the first tap. Desktop is unchanged.
|
|
336
|
+
- **3.6.0:** adds the JSON config block, `copy`, `productSummary`, CSS variables and `classes`. Existing installs need no changes: without the new settings the widget renders and behaves as 3.5.
|
|
337
|
+
- **3.0.0:** the default auth moved from `"token"` to `"proxy"`, and the bundled access token was removed. Set up the App Proxy or supply your own token.
|
|
338
|
+
- **2.0.0:** the bundle moved from `index.js` to `build/notify-me-wl.min.js`.
|
|
339
|
+
- **Legacy `#popup-open` button:** older installs put their attributes on a `<button id="popup-open">`. This still works, and its attributes win over everything else. New installs should use `data-twc-notify-me` triggers instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e="data-twc-product";function n(e){if(!e||"object"!=typeof e)return!1;const n=e;return("number"==typeof n.id||"string"==typeof n.id)&&"string"==typeof n.title&&(void 0===n.variants||Array.isArray(n.variants))}function t(){const e=new URLSearchParams(window.location.search).get("variant"),n=document.querySelector('form[action*="/cart/add"] select[name="id"]');return[e,n?n.value:null]}function o(e){return e.replace(/^data-/,"").replace(/-([a-z])/g,(e,n)=>n.toUpperCase())}function a(e,n){console.warn(`notify-me-wl: ignoring data-twc-config "${e}": expected ${n}`)}function r(e){return Array.isArray(e)&&e.every(e=>"string"==typeof e)}const c={title:".twc-nm-title",intro:".twc-nm-text",label:".twc-nm-label",input:".twc-nm-input",select:".twc-nm-select",checkbox:".twc-nm-check input",submit:".twc-nm-submit",close:".twc-nm-close",disclaimer:".twc-nm-disclaimer",summary:".twc-nm-summary",summaryImage:".twc-nm-summary-image",summaryName:".twc-nm-summary-name",summaryVariant:".twc-nm-summary-variant",summaryPrice:".twc-nm-summary-price",summaryCompareAt:".twc-nm-summary-compare"},i=/^[A-Za-z_-][A-Za-z0-9_-]*$/;function m(e){const n={};if(null==e)return n;if("object"!=typeof e||Array.isArray(e))return a("classes","an object"),n;const t=e;return Object.keys(c).forEach(e=>{const o=t[e];if(null==o)return;if("string"!=typeof o)return void a(`classes.${e}`,"a string");const r=o.split(/\s+/).filter(Boolean),c=r.filter(e=>i.test(e));c.length<r.length&&a(`classes.${e}`,'class names of letters, digits, "-" and "_"'),c.length&&(n[e]=c)}),n}const s=[{code:"AF",name:"Afghanistan"},{code:"AX",name:"Åland Islands"},{code:"AL",name:"Albania"},{code:"DZ",name:"Algeria"},{code:"AS",name:"American Samoa"},{code:"AD",name:"Andorra"},{code:"AO",name:"Angola"},{code:"AI",name:"Anguilla"},{code:"AG",name:"Antigua and Barbuda"},{code:"AR",name:"Argentina"},{code:"AM",name:"Armenia"},{code:"AW",name:"Aruba"},{code:"AU",name:"Australia"},{code:"AT",name:"Austria"},{code:"AZ",name:"Azerbaijan"},{code:"BS",name:"Bahamas"},{code:"BH",name:"Bahrain"},{code:"BD",name:"Bangladesh"},{code:"BB",name:"Barbados"},{code:"BY",name:"Belarus"},{code:"BE",name:"Belgium"},{code:"BZ",name:"Belize"},{code:"BJ",name:"Benin"},{code:"BM",name:"Bermuda"},{code:"BT",name:"Bhutan"},{code:"BO",name:"Bolivia"},{code:"BA",name:"Bosnia and Herzegovina"},{code:"BW",name:"Botswana"},{code:"BR",name:"Brazil"},{code:"BN",name:"Brunei"},{code:"BG",name:"Bulgaria"},{code:"BF",name:"Burkina Faso"},{code:"BI",name:"Burundi"},{code:"CV",name:"Cabo Verde"},{code:"KH",name:"Cambodia"},{code:"CM",name:"Cameroon"},{code:"CA",name:"Canada"},{code:"KY",name:"Cayman Islands"},{code:"CF",name:"Central African Republic"},{code:"TD",name:"Chad"},{code:"CL",name:"Chile"},{code:"CN",name:"China"},{code:"CO",name:"Colombia"},{code:"KM",name:"Comoros"},{code:"CG",name:"Congo"},{code:"CD",name:"Congo, DR"},{code:"CK",name:"Cook Islands"},{code:"CR",name:"Costa Rica"},{code:"HR",name:"Croatia"},{code:"CU",name:"Cuba"},{code:"CW",name:"Curaçao"},{code:"CY",name:"Cyprus"},{code:"CZ",name:"Czech Republic"},{code:"DK",name:"Denmark"},{code:"DJ",name:"Djibouti"},{code:"DM",name:"Dominica"},{code:"DO",name:"Dominican Republic"},{code:"EC",name:"Ecuador"},{code:"EG",name:"Egypt"},{code:"SV",name:"El Salvador"},{code:"GQ",name:"Equatorial Guinea"},{code:"ER",name:"Eritrea"},{code:"EE",name:"Estonia"},{code:"SZ",name:"Eswatini"},{code:"ET",name:"Ethiopia"},{code:"FO",name:"Faroe Islands"},{code:"FJ",name:"Fiji"},{code:"FI",name:"Finland"},{code:"FR",name:"France"},{code:"GF",name:"French Guiana"},{code:"PF",name:"French Polynesia"},{code:"GA",name:"Gabon"},{code:"GM",name:"Gambia"},{code:"GE",name:"Georgia"},{code:"DE",name:"Germany"},{code:"GH",name:"Ghana"},{code:"GI",name:"Gibraltar"},{code:"GR",name:"Greece"},{code:"GL",name:"Greenland"},{code:"GD",name:"Grenada"},{code:"GP",name:"Guadeloupe"},{code:"GU",name:"Guam"},{code:"GT",name:"Guatemala"},{code:"GN",name:"Guinea"},{code:"GW",name:"Guinea-Bissau"},{code:"GY",name:"Guyana"},{code:"HT",name:"Haiti"},{code:"HN",name:"Honduras"},{code:"HK",name:"Hong Kong"},{code:"HU",name:"Hungary"},{code:"IS",name:"Iceland"},{code:"IN",name:"India"},{code:"ID",name:"Indonesia"},{code:"IR",name:"Iran"},{code:"IQ",name:"Iraq"},{code:"IE",name:"Ireland"},{code:"IM",name:"Isle of Man"},{code:"IL",name:"Israel"},{code:"IT",name:"Italy"},{code:"JM",name:"Jamaica"},{code:"JP",name:"Japan"},{code:"JE",name:"Jersey"},{code:"JO",name:"Jordan"},{code:"KZ",name:"Kazakhstan"},{code:"KE",name:"Kenya"},{code:"KI",name:"Kiribati"},{code:"KR",name:"Korea, South"},{code:"XK",name:"Kosovo"},{code:"KW",name:"Kuwait"},{code:"KG",name:"Kyrgyzstan"},{code:"LA",name:"Laos"},{code:"LV",name:"Latvia"},{code:"LB",name:"Lebanon"},{code:"LS",name:"Lesotho"},{code:"LR",name:"Liberia"},{code:"LY",name:"Libya"},{code:"LI",name:"Liechtenstein"},{code:"LT",name:"Lithuania"},{code:"LU",name:"Luxembourg"},{code:"MO",name:"Macao"},{code:"MG",name:"Madagascar"},{code:"MW",name:"Malawi"},{code:"MY",name:"Malaysia"},{code:"MV",name:"Maldives"},{code:"ML",name:"Mali"},{code:"MT",name:"Malta"},{code:"MH",name:"Marshall Islands"},{code:"MQ",name:"Martinique"},{code:"MR",name:"Mauritania"},{code:"MU",name:"Mauritius"},{code:"MX",name:"Mexico"},{code:"FM",name:"Micronesia"},{code:"MD",name:"Moldova"},{code:"MC",name:"Monaco"},{code:"MN",name:"Mongolia"},{code:"ME",name:"Montenegro"},{code:"MS",name:"Montserrat"},{code:"MA",name:"Morocco"},{code:"MZ",name:"Mozambique"},{code:"MM",name:"Myanmar"},{code:"NA",name:"Namibia"},{code:"NR",name:"Nauru"},{code:"NP",name:"Nepal"},{code:"NL",name:"Netherlands"},{code:"NC",name:"New Caledonia"},{code:"NZ",name:"New Zealand"},{code:"NI",name:"Nicaragua"},{code:"NE",name:"Niger"},{code:"NG",name:"Nigeria"},{code:"NU",name:"Niue"},{code:"MK",name:"North Macedonia"},{code:"NO",name:"Norway"},{code:"OM",name:"Oman"},{code:"PK",name:"Pakistan"},{code:"PW",name:"Palau"},{code:"PS",name:"Palestine"},{code:"PA",name:"Panama"},{code:"PG",name:"Papua New Guinea"},{code:"PY",name:"Paraguay"},{code:"PE",name:"Peru"},{code:"PH",name:"Philippines"},{code:"PL",name:"Poland"},{code:"PT",name:"Portugal"},{code:"PR",name:"Puerto Rico"},{code:"QA",name:"Qatar"},{code:"RE",name:"Réunion"},{code:"RO",name:"Romania"},{code:"RW",name:"Rwanda"},{code:"KN",name:"Saint Kitts and Nevis"},{code:"LC",name:"Saint Lucia"},{code:"VC",name:"Saint Vincent and the Grenadines"},{code:"WS",name:"Samoa"},{code:"SM",name:"San Marino"},{code:"SA",name:"Saudi Arabia"},{code:"SN",name:"Senegal"},{code:"RS",name:"Serbia"},{code:"SC",name:"Seychelles"},{code:"SL",name:"Sierra Leone"},{code:"SG",name:"Singapore"},{code:"SK",name:"Slovakia"},{code:"SI",name:"Slovenia"},{code:"SB",name:"Solomon Islands"},{code:"SO",name:"Somalia"},{code:"ZA",name:"South Africa"},{code:"SS",name:"South Sudan"},{code:"ES",name:"Spain"},{code:"LK",name:"Sri Lanka"},{code:"SD",name:"Sudan"},{code:"SR",name:"Suriname"},{code:"SE",name:"Sweden"},{code:"CH",name:"Switzerland"},{code:"SY",name:"Syria"},{code:"TW",name:"Taiwan"},{code:"TJ",name:"Tajikistan"},{code:"TZ",name:"Tanzania"},{code:"TH",name:"Thailand"},{code:"TL",name:"Timor-Leste"},{code:"TG",name:"Togo"},{code:"TO",name:"Tonga"},{code:"TT",name:"Trinidad and Tobago"},{code:"TN",name:"Tunisia"},{code:"TR",name:"Turkey"},{code:"TM",name:"Turkmenistan"},{code:"TC",name:"Turks and Caicos Islands"},{code:"TV",name:"Tuvalu"},{code:"UG",name:"Uganda"},{code:"UA",name:"Ukraine"},{code:"AE",name:"United Arab Emirates"},{code:"GB",name:"United Kingdom"},{code:"US",name:"United States"},{code:"UY",name:"Uruguay"},{code:"UZ",name:"Uzbekistan"},{code:"VU",name:"Vanuatu"},{code:"VE",name:"Venezuela"},{code:"VN",name:"Vietnam"},{code:"VG",name:"Virgin Islands, British"},{code:"VI",name:"Virgin Islands, U.S."},{code:"YE",name:"Yemen"},{code:"ZM",name:"Zambia"},{code:"ZW",name:"Zimbabwe"}];let l=null;async function d(e){if("proxy"===e.mode){const n=await async function(e){if(l)return l;try{const n=await fetch(`/apps/${e}/auth/token`,{method:"GET",headers:{Accept:"application/json"}}),t=await n.json().catch(()=>null);return n.ok&&t&&t.success&&t.data&&t.data.access_token?(l=t.data.access_token,l):null}catch(e){return console.error("Error fetching proxy access token:",e),null}}(e.proxyApp);return n?`Bearer ${n}`:null}return e.accessToken?/^Bearer /i.test(e.accessToken)?e.accessToken:`Bearer ${e.accessToken}`:(console.error('notify-me-wl: data-auth="token" requires an access token. Set data-access-token on the open button or window.TWC_ACCESS_TOKEN, or use data-auth="proxy" to obtain one via the Shopify App Proxy.'),null)}const u=new Map;function p(e){return"string"==typeof e?e.trim():""}async function w(e,n,t){const o=`${e}|${t||""}`,a=u.get(o);if(void 0!==a)return a;const r=await d(n);if(!r)return null;try{const n=await fetch("https://api.au-aws.thewishlist.io/services/eventcollector/api/v1/custom/configs/public",{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":e}});if(!n.ok)return console.error("Tenant config request failed:",n.status),null;const a=function(e,n=null){if(!e||"object"!=typeof e)return null;const t=e.websiteRecommendations;if(!t||"object"!=typeof t)return null;const o=p(t.engine).toUpperCase();if("TWC"===o)return{engine:"TWC"};if("ATHOS"===o){const e=p(t.siteIdentifier);return e?{engine:"ATHOS",siteIdentifier:e,profileTag:p(t.profileTag)||p(t.tags)||p(n)||"similar"}:null}return null}(await n.json().catch(()=>null),t);return u.set(o,a),a}catch(e){return console.error("Error fetching tenant config:",e),null}}function f(e){return"string"==typeof e?e.trim():""}function y(e){if("number"==typeof e&&Number.isFinite(e))return e;if("string"==typeof e){const n=parseFloat(e);if(Number.isFinite(n))return n}return null}function h(e){return Boolean(e.name&&e.imageUrl&&e.productUrl)}function g(e,n){const t=new Map;return e.forEach(e=>{const n=t.get(e.product.ref);n&&n.score>=e.score||t.set(e.product.ref,e)}),Array.from(t.values()).sort((e,n)=>n.score-e.score).slice(0,n).map(e=>e.product)}async function v(e){const{email:n,tenant:t,auth:o,count:a}=e,r=await d(o);if(!r)return null;const c=Math.min(3*a,20),i=`https://api.au-aws.thewishlist.io/services/recommendations/api/v1/recommendations/${encodeURIComponent(t)}/${encodeURIComponent(n)}?n=${c}`;try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":t}});if(!e.ok)return console.error("Recommendations request failed:",e.status),null;return function(e,n){const t=e&&e.recommendations;if(!Array.isArray(t))return[];const o=[];return t.forEach(e=>{var n;const t=e&&e.product;if(!t)return;const a=f(t.name),r=f(t.image_url),c=f(t.product_url);if(!h({name:a,imageUrl:r,productUrl:c}))return;const i=f(t.product_ref)||f(t.product_id)||c;o.push({score:"number"==typeof e.score?e.score:0,product:{id:f(t.product_id)||i,ref:i,name:a,imageUrl:r,productUrl:c,price:null!==(n=y(t.price))&&void 0!==n?n:0,originalPrice:y(t.original_price)}})}),g(o,n)}(await e.json().catch(()=>null),a)}catch(e){return console.error("Error fetching recommendations:",e),null}}async function b(e){const{engine:n,email:t,count:o,productId:a}=e,r=encodeURIComponent(n.siteIdentifier),c=[`tags=${encodeURIComponent(n.profileTag)}`,`limits=${o}`];a&&c.push(`products=${encodeURIComponent(a)}`),t&&c.push(`shopper=${encodeURIComponent(t)}`);const i=`https://${r}.a.searchspring.io/boost/${r}/recommend?`+c.join("&");try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json"}});if(!e.ok)return console.error("Athos recommendations request failed:",e.status),null;return function(e,n){const t=Array.isArray(e)?e[0]:e,o=t&&t.results;if(!Array.isArray(o))return[];const a=[];return o.forEach((e,n)=>{var t;const r=e&&e.mappings&&e.mappings.core;if(!r)return;const c=f(r.name),i=f(r.imageUrl)||f(r.thumbnailImageUrl),m=f(r.url);if(!h({name:c,imageUrl:i,productUrl:m}))return;const s=f(r.uid)||f(r.sku)||m,l=null!==(t=y(r.price))&&void 0!==t?t:0,d=y(r.msrp);a.push({score:o.length-n,product:{id:f(r.uid)||s,ref:s,name:c,imageUrl:i,productUrl:m,price:l,originalPrice:null!==d&&d>l?d:null}})}),g(a,n)}(await e.json().catch(()=>null),o)}catch(e){return console.error("Error fetching Athos recommendations:",e),null}}const x=new Map;const C="twc-nm-recs",k="twc-nm-recs-title";function S(e,n){if(n)try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n}).format(e)}catch(e){}return new Intl.NumberFormat(void 0,{minimumFractionDigits:0,maximumFractionDigits:2}).format(e)}function E(){const e=document.createElement("section");e.id=C,e.className="twc-nm-recs",e.setAttribute("aria-labelledby",k);const n=document.createElement("h3");n.id=k,n.className="twc-nm-recs-title",n.textContent="Shop similar styles",e.appendChild(n);const t=document.createElement("div");return t.className="twc-nm-recs-row",function(e){let n=0,t=0,o=!1,a=!1,r=!1;e.addEventListener("pointerdown",c=>{"mouse"===c.pointerType&&0===c.button&&(o=!0,a=!1,r=!1,n=c.clientX,t=e.scrollLeft,e.classList.add("twc-nm-recs-row--dragging"))}),e.addEventListener("pointermove",r=>{if(!o)return;const c=r.clientX-n;!a&&Math.abs(c)<5||(a=!0,e.scrollLeft=t-c,r.preventDefault())});const c=()=>{o&&(o=!1,r=a,e.classList.remove("twc-nm-recs-row--dragging"))};e.addEventListener("pointerup",c),e.addEventListener("pointercancel",c),e.addEventListener("pointerleave",c),e.addEventListener("click",e=>{r&&(r=!1,e.preventDefault(),e.stopPropagation())},!0),e.addEventListener("dragstart",e=>{o&&a&&e.preventDefault()})}(t),e.appendChild(t),{section:e,row:t}}function A(){const e=document.createElement("div");e.className="twc-nm-rec twc-nm-rec--skeleton",e.setAttribute("aria-hidden","true");const n=document.createElement("div");n.className="twc-nm-rec-imgbox twc-nm-skel",e.appendChild(n);const t=document.createElement("span");t.className="twc-nm-skel twc-nm-skel-line",e.appendChild(t);const o=document.createElement("span");return o.className="twc-nm-skel twc-nm-skel-line twc-nm-skel-line--short",e.appendChild(o),e}function L(e,n,t){const o=e.querySelector(".twc-nm-recs-row");if(!o)return;const a=(r=t,window.Shopify&&window.Shopify.currency&&window.Shopify.currency.active||r||null);var r;const c=document.createDocumentFragment();n.forEach(e=>{c.appendChild(function(e,n){const t=document.createElement("a");t.className="twc-nm-rec",t.href=e.productUrl,t.target="_blank",t.rel="noopener noreferrer";const o=document.createElement("div");o.className="twc-nm-rec-imgbox";const a=document.createElement("img");a.className="twc-nm-rec-img",a.src=e.imageUrl,a.alt=e.name,a.loading="lazy",a.decoding="async",o.appendChild(a),t.appendChild(o);const r=document.createElement("span");r.className="twc-nm-rec-name",r.textContent=e.name,t.appendChild(r);const c=document.createElement("span");if(c.className="twc-nm-rec-price",null!==e.originalPrice&&e.originalPrice>e.price){const t=document.createElement("s");t.className="twc-nm-rec-was",t.textContent=S(e.originalPrice,n),c.appendChild(t),c.appendChild(document.createTextNode(" "))}return c.appendChild(document.createTextNode(S(e.price,n))),t.appendChild(c),t}(e,a))}),o.textContent="",o.appendChild(c),e.classList.remove("twc-nm-recs--loading"),e.removeAttribute("aria-busy")}function T(){const e=document.getElementById(C);e&&e.parentNode&&e.parentNode.removeChild(e)}const N={"notify-me":{intro:"Register to receive a notification as soon as this item is back in stock",submit:"Notify Me"},"coming-soon":{intro:"Register your interest to hear more about this item",submit:"Register Interest"}},M=["title","intro","sizeLabel","sizePlaceholder","emailPlaceholder","emailOptIn","smsOptIn","submit","success","disclaimer"];function I(e,n){const t={title:"{product}",intro:N[n].intro,sizeLabel:"Size",sizePlaceholder:"Select a size",emailLabel:"Email",emailPlaceholder:"you@example.com",emailOptIn:"Email me store updates",smsOptIn:"Text me store updates",submit:N[n].submit,success:"You're on the list. We'll be in touch.",disclaimer:null};if(void 0===e)return t;if(!e||"object"!=typeof e||Array.isArray(e))return a("copy","an object"),t;const o=e;M.forEach(e=>{const n=o[e];null!=n&&("string"==typeof n?t[e]=n:a(`copy.${e}`,"a string"))});const r=o.emailLabel;return!1===r||"string"==typeof r?t.emailLabel=r:null!=r&&a("copy.emailLabel","a string or false"),t}function B(e,n){return e.split("{product}").join(n)}function P(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function $(e){if("string"!=typeof e)return null;const n=e.trim();return/^(https?:)?\/\//i.test(n)?n:null}function z(e){return"string"==typeof e?e.trim():""}function G(o){const{wrapper:a,config:r,localization:i}=o,{fields:m,type:l,display:u,auth:p,tenant:f,recommendationsEnabled:y,recommendationsCount:h,recommendationsProfile:g,currency:k,customerEmail:S,copy:N,productSummary:M,classes:I,jsonConfigured:G}=r,U='<button type="button" id="popup-close" class="twc-nm-close" aria-label="Close"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" fill="none"/></svg></button>',j='<h2 id="popup-title" class="twc-nm-title"></h2>',R=G?`<div class="twc-nm-header">${U}${j}</div>`:`${U}\n ${j}`,O=document.createElement("div");O.innerHTML=`\n <div id="twc-nm-overlay" role="dialog" aria-modal="true" aria-labelledby="popup-title">\n <div class="twc-nm-card">\n ${R}\n <p id="popup-text" class="twc-nm-text"></p>\n <div id="popup-notice" class="twc-nm-status" role="status" aria-live="polite" hidden></div>\n <form id="popup-form" class="twc-nm-form">\n <div class="twc-nm-field">\n <label class="twc-nm-label" for="twc-nm-size">${P(N.sizeLabel)}</label>\n <select class="twc-nm-select" id="twc-nm-size" name="select-size" required>\n <option value="" disabled selected>${P(N.sizePlaceholder)}</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n `,a.appendChild(O);const H=document.getElementById("twc-nm-overlay"),F=document.getElementById("popup-close"),K=document.getElementById("popup-title"),D=document.getElementById("popup-text"),q=document.querySelector("select[name='select-size']"),_=document.getElementById("popup-form"),V=document.getElementById("popup-notice");if(!(H&&F&&K&&D&&q&&_&&V))return null;"panel"===u&&H.classList.add("twc-nm--panel");const J=M?function(){const e=document.createElement("div");return e.className="twc-nm-summary",e.innerHTML='<div class="twc-nm-summary-image"><img alt="" /></div><div class="twc-nm-summary-info"><p class="twc-nm-summary-name"></p><p class="twc-nm-summary-variant"></p><p class="twc-nm-summary-price"><span class="twc-nm-summary-now"></span> <s class="twc-nm-summary-compare"></s></p></div>',e}():null;J&&_.insertBefore(J,_.firstChild);const Z=s.map(e=>`<option value="${e.code}"${e.code===i.countryCode?" selected":""}>${e.name}</option>`).join(""),W=(e,n,t)=>`<div class="twc-nm-field"><label class="twc-nm-label" for="${e}">${n}</label>${t}</div>`,Y={email:!1===N.emailLabel?`<div class="twc-nm-field"><input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(N.emailPlaceholder)}" aria-label="${P(N.emailPlaceholder||"Email")}" required /></div>`:W("twc-nm-email",P(N.emailLabel),`<input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(N.emailPlaceholder)}" required />`),mobile:W("twc-nm-mobile","Mobile",'<input class="twc-nm-input" id="twc-nm-mobile" name="mobile" type="tel" placeholder="Mobile number" required />'),firstName:W("twc-nm-firstname","First name",'<input class="twc-nm-input" id="twc-nm-firstname" name="firstName" type="text" placeholder="First name" required />'),lastName:W("twc-nm-lastname","Last name",'<input class="twc-nm-input" id="twc-nm-lastname" name="lastName" type="text" placeholder="Last name" required />'),countryCode:W("twc-nm-country","Country",`<select class="twc-nm-select" id="twc-nm-country" name="countryCode"><option value="">Select country</option>${Z}</select>`),provinceCode:W("twc-nm-province","State / Province",`<input class="twc-nm-input" id="twc-nm-province" name="provinceCode" type="text" placeholder="State / province code" value="${i.provinceCode||""}" />`)};m.forEach(e=>{Y[e]&&_.insertAdjacentHTML("beforeend",Y[e])});const X=[m.includes("email")?`<label class="twc-nm-check"><input type="checkbox" name="mailList" /> ${P(N.emailOptIn)}</label>`:"",m.includes("mobile")?`<label class="twc-nm-check"><input type="checkbox" name="mailListSms" /> ${P(N.smsOptIn)}</label>`:""].join("");if(X&&_.insertAdjacentHTML("beforeend",`<div class="twc-nm-checks">${X}</div>`),_.insertAdjacentHTML("beforeend",'<div id="popup-status" class="twc-nm-status" role="status" aria-live="polite" hidden></div>'),_.insertAdjacentHTML("beforeend",`<button type="submit" class="twc-nm-submit">${P(N.submit)}</button>`),N.disclaimer&&_.insertAdjacentHTML("beforeend",`<p class="twc-nm-disclaimer">${P(N.disclaimer)}</p>`),function(e,n){Object.keys(n).forEach(t=>{const o=n[t];o&&e.querySelectorAll(c[t]).forEach(e=>{e.classList.add(...o,"twc-nm-themed")})})}(H,I),S){const e=_.querySelector("input[name='email']");e&&(e.value=S)}const Q=document.getElementById("popup-status"),ee=_.querySelector(".twc-nm-submit");if(!Q||!ee)return null;function ne(e,n){Q.textContent=n,Q.className=`twc-nm-status twc-nm-status--${e}`,Q.hidden=!1}function te(){Q.hidden=!0,Q.textContent=""}function oe(e,n){_.hidden=!0,V.textContent=n,V.className=`twc-nm-status twc-nm-status--${e}`,V.hidden=!1}const ae=e=>{const n=_.querySelector(e);return n?n.value:""},re=e=>{const n=_.querySelector(e);return!!n&&n.checked},ce=H.querySelector(".twc-nm-card");let ie=null,me=[],se=null;async function le(e){const n=await function(e,n,t){return w(e,n,t)}(f,p,g);if(!n)return!1;if(!H.classList.contains("is-open"))return!1;const t=function(e){const{section:n,row:t}=E();n.classList.add("twc-nm-recs--loading"),n.setAttribute("aria-busy","true");for(let n=0;n<e;n++)t.appendChild(A());return n}(h);ce.appendChild(t);const o=await async function(e,n){const{email:t,tenant:o,auth:a,count:r,productId:c}=n,i=`${e.engine}|${o}|${t}|${r}|${c||""}`,m=x.get(i);if(m)return m;let s=null;return s="ATHOS"===e.engine?await b({engine:e,email:t,count:r,productId:c}):await v({email:t,tenant:o,auth:a,count:r}),null===s?[]:(x.set(i,s),s)}(n,{email:e,tenant:f,auth:p,count:h,productId:ie?String(ie.id):null});return!!t.isConnected&&(o.length?(L(t,o,k),!0):(T(),!1))}function de(e){if(!y||!ce)return Promise.resolve(!1);if(se)return se;if(document.getElementById(C))return Promise.resolve(!0);const n=le(e);return se=n,n.then(e=>(se===n&&(se=null),e),e=>(se===n&&(se=null),console.error("Error rendering recommendations:",e),!1))}let ue=null,pe=0;function we(e){"Escape"===e.key&&fe()}function fe(){pe++,H.classList.remove("is-open"),document.body.style.overflow="",document.removeEventListener("keydown",we),te(),T(),se=null,ue&&"function"==typeof ue.focus&&ue.focus()}function ye(){if(!J||!ie)return;const e=me.find(e=>String(e.id)===q.value)||null;!function(e,n,t,o){const a=n=>e.querySelector(n),r=a(".twc-nm-summary-image"),c=a(".twc-nm-summary-image img"),i=t&&$(t.image)||$(n.image);i?(c.src=i,c.alt=n.title):c.removeAttribute("src"),r.hidden=!i,a(".twc-nm-summary-name").textContent=n.title;const m=a(".twc-nm-summary-variant");m.textContent=t?t.title:"",m.hidden=!t;let s="",l="";if(t)s=z(t.price),l=z(t.compareAtPrice);else{const e=o.map(e=>z(e.price));e.length&&e[0]&&e.every(n=>n===e[0])&&(s=e[0])}l===s&&(l=""),a(".twc-nm-summary-now").textContent=s;const d=a(".twc-nm-summary-compare");d.textContent=l,d.hidden=!l,a(".twc-nm-summary-price").hidden=!s}(J,ie,e,me)}return F.addEventListener("click",fe),q.addEventListener("change",ye),H.addEventListener("click",e=>{e.target===H&&fe()}),_.addEventListener("submit",async e=>{e.preventDefault();const n=me.find(e=>String(e.id)===q.value);if(!n)return void ne("error","Please select a size.");const t={variantRef:n.id,email:ae("input[name='email']"),subscribe:re("input[name='mailList']"),subscribeSms:re("input[name='mailListSms']")};if("coming-soon"===l?t.comingSoon=!0:t.notifyMe=!0,m.includes("firstName")&&(t.firstName=ae("input[name='firstName']")),m.includes("lastName")&&(t.lastName=ae("input[name='lastName']")),m.includes("mobile")&&(t.mobile=ae("input[name='mobile']")),m.includes("countryCode")){const e=ae("select[name='countryCode']");e&&(t.countryCode=e)}else i.countryCode&&(t.countryCode=i.countryCode);if(m.includes("provinceCode")){const e=ae("input[name='provinceCode']");e&&(t.provinceCode=e)}else i.provinceCode&&(t.provinceCode=i.provinceCode);i.marketId&&(t.marketId=i.marketId),i.marketHandle&&(t.marketHandle=i.marketHandle),te();const o=pe,a=ee.textContent||"";ee.disabled=!0,ee.textContent="Sending…";let r={ok:!1,reason:"error"};try{r=await async function(e,n,t){const o=await d(n);if(!o)return{ok:!1,reason:"auth"};try{return(await fetch("https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest",{method:"POST",headers:{"Content-Type":"application/json",Authorization:o,"X-Twc-Tenant":t},body:JSON.stringify(e)})).ok?{ok:!0}:{ok:!1,reason:"error"}}catch(e){return console.error("Error submitting form:",e),{ok:!1,reason:"error"}}}(t,p,f)}finally{o===pe&&(ee.disabled=!1,ee.textContent=a)}if(o===pe)if(r.ok){ne("success",N.success);const e=S||ae("input[name='email']");!!e&&await de(e)||o!==pe||setTimeout(()=>{o===pe&&fe()},1500)}else"auth"===r.reason?ne("error","proxy"===p.mode?"Please log in to your account to continue.":"Something went wrong. Please try again."):ne("error","Something went wrong. Please try again.")}),{open:function(o){pe++,ee.disabled=!1,ee.textContent=N.submit,te(),_.hidden=!1,V.hidden=!0,V.textContent="",T(),se=null,H.classList.contains("is-open")||(ue=document.activeElement),H.classList.add("is-open"),document.body.style.overflow="hidden",document.addEventListener("keydown",we);const a=function(t){const o=t?t.closest(`[${e}]`):null;if(o){try{const t=JSON.parse(o.getAttribute(e)||"");if(n(t))return{ok:!0,product:t,source:"card"}}catch(e){}return console.error(`notify-me-wl: invalid ${e} JSON on`,o),{ok:!1}}const a=window.currentProduct;return n(a)?{ok:!0,product:a,source:"page"}:{ok:!1}}(o);if(!a.ok)return ie=null,me=[],K.textContent="",D.textContent=B(N.intro,""),void oe("error","Product information is unavailable. Please refresh the page.");if(ie=a.product,me=(ie.variants||[]).filter(e=>!!e&&("number"==typeof e.id||"string"==typeof e.id)&&"string"==typeof e.title&&!0!==e.available),K.textContent=B(N.title,ie.title),D.textContent=B(N.intro,ie.title),!me.length)return void oe("info","All sizes are currently in stock.");const r=o?o.getAttribute("data-variant-id"):null,c="page"===a.source?[r,...t()]:[r];var i,m;i=me,m=function(e,n){const t=e.map(e=>String(e.id));for(const e of n){const n=e?e.trim():"";if(n&&t.includes(n))return n}return 1===t.length?t[0]:null}(me,c),q.innerHTML=`<option value="" disabled selected>${P(N.sizePlaceholder)}</option>`,i.forEach(e=>{const n=document.createElement("option");n.value=String(e.id),n.textContent=e.title,q.appendChild(n)}),m&&(q.value=m),ye(),S&&de(S);const s=_.querySelector("select, input");s&&s.focus()}}}function U(e){var n;const t=(null!=e?e:"").trim();if(!t)return null;const o=null!==(n=t.split("?")[0].split("/").pop())&&void 0!==n?n:"";return/^\d+$/.test(o)?o:null}function j(e){const n=(null!=e?e:"").trim();return/^[a-z0-9_-]+$/i.test(n)?n:null}function R(e){const n=(null!=e?e:"").trim();return/^[a-z]{2}$/i.test(n)?n.toUpperCase():null}function O(e){var n,t,o;const a={countryCode:R(e("data-country-code")),provinceCode:null,marketId:U(e("data-market-id")),marketHandle:j(e("data-market-handle"))},r=window.Shopify;if(r){const e=null===(n=r.markets)||void 0===n?void 0:n.currentMarket;if(e&&(!a.countryCode&&e.countryCode&&(a.countryCode=e.countryCode),a.marketId||(a.marketId=U(String(null!==(t=e.id)&&void 0!==t?t:"")))),a.countryCode||"string"!=typeof r.country||(a.countryCode=r.country),!a.countryCode&&"string"==typeof r.locale){const e=r.locale.split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}}if(!a.countryCode||!a.provinceCode){const e=null===(o=window.customer)||void 0===o?void 0:o.default_address;e&&(!a.countryCode&&e.country_code&&(a.countryCode=e.country_code),!a.provinceCode&&e.province_code&&(a.provinceCode=e.province_code))}if(!a.countryCode){const e=(navigator.language||"").split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}return a}function H(e){const n=e("data-customer-email");if(n&&n.trim())return n.trim();const t=window.customer&&window.customer.email;if(t&&t.trim())return t.trim();const o=window.customerEmail;return"string"==typeof o&&o.trim()?o.trim():null}const F="[data-twc-notify-me]";function K(){const e=document.getElementById("notification-widget");if(!e)return;const n=document.getElementById("popup-open"),t=function(e){const n=e.querySelector('script[type="application/json"][data-twc-config]');if(!n)return null;try{const e=JSON.parse(n.textContent||"");if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch(e){}return console.error("notify-me-wl: invalid data-twc-config JSON"),null}(e),c=function(e,n,t){const c=t=>(e?e.getAttribute(t):null)||n.getAttribute(t)||null,i=e=>null===t[e]?void 0:t[e],m=e=>{const n=i(e);return void 0===n?null:"boolean"==typeof n?n:(a(e,"true or false"),null)};return{string(e){const n=c(e);if(null!==n)return n;const t=o(e),r=i(t);return void 0===r?null:"string"==typeof r?r:"number"==typeof r&&Number.isFinite(r)?String(r):(a(t,"a string"),null)},boolean(e){const n=c(e);return null!==n?"false"!==n:m(o(e))},number(e){const n=c(e);if(null!==n){const e=parseInt(n,10);if(Number.isFinite(e))return e}const t=o(e),r=i(t);return void 0===r?null:"number"==typeof r&&Number.isFinite(r)?r:(a(t,"a number"),null)},stringArray(e){const n=c(e);if(null!==n){try{const e=JSON.parse(n);if(r(e))return e}catch(e){}console.warn(`notify-me-wl: ignoring invalid ${e} attribute`)}const t=o(e),m=i(t);return void 0===m?null:r(m)?m:(a(t,"an array of strings"),null)},json:i,jsonBoolean:m}}(n,e,t||{}),i=c.string;!function(){const e=document.createElement("style");e.type="text/css",e.innerText="\n .notification-btn {\n display: inline-block;\n width: 100%;\n padding: 12px 24px;\n background: #fff;\n border: 1px solid #111827;\n border-radius: 8px;\n color: #111827;\n font: inherit;\n text-align: center;\n text-transform: uppercase;\n cursor: pointer;\n }\n .notification-btn:hover {\n background: #111827;\n color: #fff;\n }\n #twc-nm-overlay {\n position: fixed;\n inset: 0;\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: var(--twc-nm-overlay-bg, rgba(17, 17, 17, 0.55));\n opacity: 0;\n visibility: hidden;\n transition: opacity var(--twc-nm-transition, 200ms ease);\n }\n #twc-nm-overlay.is-open {\n opacity: 1;\n visibility: visible;\n }\n #twc-nm-overlay * {\n box-sizing: border-box;\n }\n #twc-nm-overlay .twc-nm-card {\n position: relative;\n width: 100%;\n max-width: 400px;\n max-height: calc(100vh - 32px);\n overflow-y: auto;\n padding: var(--twc-nm-padding, 24px);\n background: var(--twc-nm-bg, #fff);\n color: var(--twc-nm-text, #111827);\n border-radius: 12px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);\n font: inherit;\n font-family: var(--twc-nm-font, inherit);\n transform: translateY(8px);\n transition: transform var(--twc-nm-transition, 200ms ease);\n }\n #twc-nm-overlay.is-open .twc-nm-card {\n transform: translateY(0);\n }\n /* Panel mode: same blocking backdrop, docked to the right edge and\n full height instead of centred. Declared after the modal rules so\n the shared properties are overridden. */\n #twc-nm-overlay.twc-nm--panel {\n align-items: stretch;\n justify-content: flex-end;\n padding: 0;\n }\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width, 420px);\n max-width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: var(--twc-nm-panel-shadow, -10px 0 40px rgba(0, 0, 0, 0.18));\n transform: translateX(100%);\n }\n /* Needs the extra class to beat both the .is-open rule above and the\n panel's own resting transform. */\n #twc-nm-overlay.twc-nm--panel.is-open .twc-nm-card {\n transform: translateX(0);\n }\n @media (max-width: 480px) {\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width-mobile, 100%);\n }\n }\n /* Groups the title and close button so a theme can lay them out as a\n row. With no border it changes nothing: the close button stays\n positioned against the card. */\n #twc-nm-overlay .twc-nm-header {\n border-bottom: var(--twc-nm-header-border, none);\n }\n /* Layout below is never themed — it keeps the close button usable\n whatever classes a theme attaches. Cosmetics follow :not(themed). */\n #twc-nm-overlay .twc-nm-close {\n position: absolute;\n top: 12px;\n right: 12px;\n width: 32px;\n height: 32px;\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n border: none;\n background: transparent;\n box-shadow: none;\n text-indent: 0;\n line-height: 1;\n opacity: 1;\n visibility: visible;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)) {\n border-radius: 6px;\n color: var(--twc-nm-muted, #6b7280);\n }\n /* The icon is an inline SVG rather than a text glyph so host-theme\n button resets (font-size: 0, icon fonts, text-indent) can't hide it. */\n #twc-nm-overlay .twc-nm-close svg {\n display: block;\n width: 16px;\n height: 16px;\n flex: none;\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-close::before,\n #twc-nm-overlay .twc-nm-close::after {\n content: none;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)):hover {\n background: #f3f4f6;\n color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-title {\n margin: 0 32px 4px 0;\n }\n #twc-nm-overlay .twc-nm-title:where(:not(.twc-nm-themed)) {\n font-size: 18px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-text {\n margin: 0 0 16px;\n }\n #twc-nm-overlay .twc-nm-text:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n #twc-nm-overlay .twc-nm-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n #twc-nm-overlay .twc-nm-label:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n font-weight: 500;\n color: #374151;\n }\n #twc-nm-overlay .twc-nm-input,\n #twc-nm-overlay .twc-nm-select {\n width: 100%;\n }\n /* The shorthand resets every background sub-property, so host CSS\n can't leak into an unconfigured widget; the select's arrow is added\n by the rule after it. */\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)),\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n color: var(--twc-nm-text, #111827);\n background: #fff;\n border: 1px solid var(--twc-nm-border, #e5e7eb);\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)):focus,\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)):focus {\n outline: none;\n border-color: var(--twc-nm-text, #111827);\n box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);\n }\n #twc-nm-overlay .twc-nm-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n }\n /* After the shared padding above, which it must override. */\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding-right: 36px;\n }\n #twc-nm-overlay .twc-nm-checks {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n #twc-nm-overlay .twc-nm-check {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #374151;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-check input {\n width: 16px;\n height: 16px;\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-check input:where(:not(.twc-nm-themed)) {\n accent-color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-status {\n font-size: 13px;\n line-height: 1.5;\n padding: 10px 12px;\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-status--error {\n color: #b91c1c;\n background: #fef2f2;\n }\n #twc-nm-overlay .twc-nm-status--success {\n color: #15803d;\n background: #f0fdf4;\n }\n #twc-nm-overlay .twc-nm-status--info {\n color: #374151;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-submit {\n width: 100%;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)) {\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n font-family: inherit;\n color: #fff;\n background: #111827;\n border: none;\n border-radius: var(--twc-nm-radius, 8px);\n transition: background 150ms ease;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)):hover {\n background: #374151;\n }\n #twc-nm-overlay .twc-nm-submit:disabled {\n opacity: 0.6;\n cursor: default;\n }\n #twc-nm-overlay .twc-nm-disclaimer {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-disclaimer:where(:not(.twc-nm-themed)) {\n font-size: 12px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary {\n display: flex;\n align-items: flex-start;\n }\n #twc-nm-overlay .twc-nm-summary:where(:not(.twc-nm-themed)) {\n gap: 16px;\n }\n /* Theme classes often set display; hidden parts must stay hidden. */\n #twc-nm-overlay .twc-nm-summary [hidden] {\n display: none !important;\n }\n #twc-nm-overlay .twc-nm-summary-image {\n flex: none;\n }\n #twc-nm-overlay .twc-nm-summary-image:where(:not(.twc-nm-themed)) {\n width: 96px;\n }\n #twc-nm-overlay .twc-nm-summary-image img {\n display: block;\n width: 100%;\n height: auto;\n }\n #twc-nm-overlay .twc-nm-summary-info {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n #twc-nm-overlay .twc-nm-summary-info p {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-summary-name {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n #twc-nm-overlay .twc-nm-summary-name:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-summary-variant:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary-price:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n }\n #twc-nm-overlay .twc-nm-summary-compare:where(:not(.twc-nm-themed)) {\n margin-left: 6px;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-recs {\n margin-top: 20px;\n padding-top: 20px;\n border-top: 1px solid var(--twc-nm-border, #e5e7eb);\n }\n #twc-nm-overlay .twc-nm-recs-title {\n margin: 0 0 12px;\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-recs-row {\n display: flex;\n gap: 12px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 8px;\n scrollbar-width: thin;\n }\n /* Only where a real pointer exists — the affordance is meaningless\n on touch, which pans the row natively. */\n @media (hover: hover) {\n #twc-nm-overlay .twc-nm-recs-row {\n cursor: grab;\n }\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging,\n #twc-nm-overlay .twc-nm-recs-row--dragging .twc-nm-rec {\n cursor: grabbing;\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging {\n /* Snapping fights a manually driven scrollLeft and feels sticky. */\n scroll-snap-type: none;\n user-select: none;\n -webkit-user-select: none;\n }\n #twc-nm-overlay .twc-nm-rec {\n flex: 0 0 132px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 6px;\n color: inherit;\n text-decoration: none;\n }\n #twc-nm-overlay .twc-nm-rec:focus-visible {\n outline: 2px solid #111827;\n outline-offset: 2px;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox {\n aspect-ratio: 3 / 4;\n overflow: hidden;\n border-radius: 8px;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-rec-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 150ms ease;\n }\n #twc-nm-overlay .twc-nm-rec:hover .twc-nm-rec-img {\n opacity: 0.85;\n }\n #twc-nm-overlay .twc-nm-rec-name {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n font-size: 13px;\n line-height: 1.35;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-price {\n font-size: 13px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-was {\n margin-right: 4px;\n font-weight: 400;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-rec--skeleton {\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n background-color: #f3f4f6;\n background-image: linear-gradient(\n 90deg,\n #f3f4f6 0%,\n #e8eaee 50%,\n #f3f4f6 100%\n );\n background-size: 200% 100%;\n border-radius: 4px;\n animation: twc-nm-shimmer 1.3s ease-in-out infinite;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox.twc-nm-skel {\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-skel-line {\n height: 12px;\n width: 100%;\n }\n #twc-nm-overlay .twc-nm-skel-line--short {\n width: 45%;\n }\n @keyframes twc-nm-shimmer {\n from {\n background-position: 200% 0;\n }\n to {\n background-position: -200% 0;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n #twc-nm-overlay,\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay .twc-nm-rec-img {\n transition: none;\n }\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n transform: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n animation: none;\n }\n }\n ",document.head.appendChild(e)}();const s=c.stringArray("data-fields")||["email"],l=i("data-type")||"notify-me",d="panel"===i("data-display")?"panel":"modal",u=i("data-auth")||"proxy",p=i("data-tenant")||"victoria-woods",w=i("data-proxy-app")||"twc-sdk",f=i("data-access-token")||window.TWC_ACCESS_TOKEN||null,y=!1!==c.boolean("data-recommendations"),h=c.number("data-recommendations-count"),g=G({wrapper:e,config:{fields:s,type:l,display:d,auth:{mode:u,proxyApp:w,accessToken:f},tenant:p,recommendationsEnabled:y,recommendationsCount:null!==h&&h>0?h:8,recommendationsProfile:(i("data-recommendations-profile")||"").trim()||null,currency:i("data-currency")||null,customerEmail:H(i),copy:I(c.json("copy"),l),productSummary:!0===c.jsonBoolean("productSummary"),classes:m(c.json("classes")),jsonConfigured:null!==t},localization:O(i)});g&&(document.addEventListener("click",e=>{const n=e.target,t=n instanceof Element?n.closest(F):null;t&&(e.preventDefault(),g.open(t))},!0),n&&!n.matches(F)&&n.addEventListener("click",()=>g.open(n)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",K):K()}();
|
|
1
|
+
!function(){"use strict";const e="data-twc-product";function n(e){if(!e||"object"!=typeof e)return!1;const n=e;return("number"==typeof n.id||"string"==typeof n.id)&&"string"==typeof n.title&&(void 0===n.variants||Array.isArray(n.variants))}function t(){const e=new URLSearchParams(window.location.search).get("variant"),n=document.querySelector('form[action*="/cart/add"] select[name="id"]');return[e,n?n.value:null]}function o(e){return e.replace(/^data-/,"").replace(/-([a-z])/g,(e,n)=>n.toUpperCase())}function a(e,n){console.warn(`notify-me-wl: ignoring data-twc-config "${e}": expected ${n}`)}function r(e){return Array.isArray(e)&&e.every(e=>"string"==typeof e)}const c={title:".twc-nm-title",intro:".twc-nm-text",label:".twc-nm-label",input:".twc-nm-input",select:".twc-nm-select",checkbox:".twc-nm-check input",submit:".twc-nm-submit",close:".twc-nm-close",disclaimer:".twc-nm-disclaimer",summary:".twc-nm-summary",summaryImage:".twc-nm-summary-image",summaryName:".twc-nm-summary-name",summaryVariant:".twc-nm-summary-variant",summaryPrice:".twc-nm-summary-price",summaryCompareAt:".twc-nm-summary-compare"},i=/^[A-Za-z_-][A-Za-z0-9_-]*$/;function s(e){const n={};if(null==e)return n;if("object"!=typeof e||Array.isArray(e))return a("classes","an object"),n;const t=e;return Object.keys(c).forEach(e=>{const o=t[e];if(null==o)return;if("string"!=typeof o)return void a(`classes.${e}`,"a string");const r=o.split(/\s+/).filter(Boolean),c=r.filter(e=>i.test(e));c.length<r.length&&a(`classes.${e}`,'class names of letters, digits, "-" and "_"'),c.length&&(n[e]=c)}),n}const m=[{code:"AF",name:"Afghanistan"},{code:"AX",name:"Åland Islands"},{code:"AL",name:"Albania"},{code:"DZ",name:"Algeria"},{code:"AS",name:"American Samoa"},{code:"AD",name:"Andorra"},{code:"AO",name:"Angola"},{code:"AI",name:"Anguilla"},{code:"AG",name:"Antigua and Barbuda"},{code:"AR",name:"Argentina"},{code:"AM",name:"Armenia"},{code:"AW",name:"Aruba"},{code:"AU",name:"Australia"},{code:"AT",name:"Austria"},{code:"AZ",name:"Azerbaijan"},{code:"BS",name:"Bahamas"},{code:"BH",name:"Bahrain"},{code:"BD",name:"Bangladesh"},{code:"BB",name:"Barbados"},{code:"BY",name:"Belarus"},{code:"BE",name:"Belgium"},{code:"BZ",name:"Belize"},{code:"BJ",name:"Benin"},{code:"BM",name:"Bermuda"},{code:"BT",name:"Bhutan"},{code:"BO",name:"Bolivia"},{code:"BA",name:"Bosnia and Herzegovina"},{code:"BW",name:"Botswana"},{code:"BR",name:"Brazil"},{code:"BN",name:"Brunei"},{code:"BG",name:"Bulgaria"},{code:"BF",name:"Burkina Faso"},{code:"BI",name:"Burundi"},{code:"CV",name:"Cabo Verde"},{code:"KH",name:"Cambodia"},{code:"CM",name:"Cameroon"},{code:"CA",name:"Canada"},{code:"KY",name:"Cayman Islands"},{code:"CF",name:"Central African Republic"},{code:"TD",name:"Chad"},{code:"CL",name:"Chile"},{code:"CN",name:"China"},{code:"CO",name:"Colombia"},{code:"KM",name:"Comoros"},{code:"CG",name:"Congo"},{code:"CD",name:"Congo, DR"},{code:"CK",name:"Cook Islands"},{code:"CR",name:"Costa Rica"},{code:"HR",name:"Croatia"},{code:"CU",name:"Cuba"},{code:"CW",name:"Curaçao"},{code:"CY",name:"Cyprus"},{code:"CZ",name:"Czech Republic"},{code:"DK",name:"Denmark"},{code:"DJ",name:"Djibouti"},{code:"DM",name:"Dominica"},{code:"DO",name:"Dominican Republic"},{code:"EC",name:"Ecuador"},{code:"EG",name:"Egypt"},{code:"SV",name:"El Salvador"},{code:"GQ",name:"Equatorial Guinea"},{code:"ER",name:"Eritrea"},{code:"EE",name:"Estonia"},{code:"SZ",name:"Eswatini"},{code:"ET",name:"Ethiopia"},{code:"FO",name:"Faroe Islands"},{code:"FJ",name:"Fiji"},{code:"FI",name:"Finland"},{code:"FR",name:"France"},{code:"GF",name:"French Guiana"},{code:"PF",name:"French Polynesia"},{code:"GA",name:"Gabon"},{code:"GM",name:"Gambia"},{code:"GE",name:"Georgia"},{code:"DE",name:"Germany"},{code:"GH",name:"Ghana"},{code:"GI",name:"Gibraltar"},{code:"GR",name:"Greece"},{code:"GL",name:"Greenland"},{code:"GD",name:"Grenada"},{code:"GP",name:"Guadeloupe"},{code:"GU",name:"Guam"},{code:"GT",name:"Guatemala"},{code:"GN",name:"Guinea"},{code:"GW",name:"Guinea-Bissau"},{code:"GY",name:"Guyana"},{code:"HT",name:"Haiti"},{code:"HN",name:"Honduras"},{code:"HK",name:"Hong Kong"},{code:"HU",name:"Hungary"},{code:"IS",name:"Iceland"},{code:"IN",name:"India"},{code:"ID",name:"Indonesia"},{code:"IR",name:"Iran"},{code:"IQ",name:"Iraq"},{code:"IE",name:"Ireland"},{code:"IM",name:"Isle of Man"},{code:"IL",name:"Israel"},{code:"IT",name:"Italy"},{code:"JM",name:"Jamaica"},{code:"JP",name:"Japan"},{code:"JE",name:"Jersey"},{code:"JO",name:"Jordan"},{code:"KZ",name:"Kazakhstan"},{code:"KE",name:"Kenya"},{code:"KI",name:"Kiribati"},{code:"KR",name:"Korea, South"},{code:"XK",name:"Kosovo"},{code:"KW",name:"Kuwait"},{code:"KG",name:"Kyrgyzstan"},{code:"LA",name:"Laos"},{code:"LV",name:"Latvia"},{code:"LB",name:"Lebanon"},{code:"LS",name:"Lesotho"},{code:"LR",name:"Liberia"},{code:"LY",name:"Libya"},{code:"LI",name:"Liechtenstein"},{code:"LT",name:"Lithuania"},{code:"LU",name:"Luxembourg"},{code:"MO",name:"Macao"},{code:"MG",name:"Madagascar"},{code:"MW",name:"Malawi"},{code:"MY",name:"Malaysia"},{code:"MV",name:"Maldives"},{code:"ML",name:"Mali"},{code:"MT",name:"Malta"},{code:"MH",name:"Marshall Islands"},{code:"MQ",name:"Martinique"},{code:"MR",name:"Mauritania"},{code:"MU",name:"Mauritius"},{code:"MX",name:"Mexico"},{code:"FM",name:"Micronesia"},{code:"MD",name:"Moldova"},{code:"MC",name:"Monaco"},{code:"MN",name:"Mongolia"},{code:"ME",name:"Montenegro"},{code:"MS",name:"Montserrat"},{code:"MA",name:"Morocco"},{code:"MZ",name:"Mozambique"},{code:"MM",name:"Myanmar"},{code:"NA",name:"Namibia"},{code:"NR",name:"Nauru"},{code:"NP",name:"Nepal"},{code:"NL",name:"Netherlands"},{code:"NC",name:"New Caledonia"},{code:"NZ",name:"New Zealand"},{code:"NI",name:"Nicaragua"},{code:"NE",name:"Niger"},{code:"NG",name:"Nigeria"},{code:"NU",name:"Niue"},{code:"MK",name:"North Macedonia"},{code:"NO",name:"Norway"},{code:"OM",name:"Oman"},{code:"PK",name:"Pakistan"},{code:"PW",name:"Palau"},{code:"PS",name:"Palestine"},{code:"PA",name:"Panama"},{code:"PG",name:"Papua New Guinea"},{code:"PY",name:"Paraguay"},{code:"PE",name:"Peru"},{code:"PH",name:"Philippines"},{code:"PL",name:"Poland"},{code:"PT",name:"Portugal"},{code:"PR",name:"Puerto Rico"},{code:"QA",name:"Qatar"},{code:"RE",name:"Réunion"},{code:"RO",name:"Romania"},{code:"RW",name:"Rwanda"},{code:"KN",name:"Saint Kitts and Nevis"},{code:"LC",name:"Saint Lucia"},{code:"VC",name:"Saint Vincent and the Grenadines"},{code:"WS",name:"Samoa"},{code:"SM",name:"San Marino"},{code:"SA",name:"Saudi Arabia"},{code:"SN",name:"Senegal"},{code:"RS",name:"Serbia"},{code:"SC",name:"Seychelles"},{code:"SL",name:"Sierra Leone"},{code:"SG",name:"Singapore"},{code:"SK",name:"Slovakia"},{code:"SI",name:"Slovenia"},{code:"SB",name:"Solomon Islands"},{code:"SO",name:"Somalia"},{code:"ZA",name:"South Africa"},{code:"SS",name:"South Sudan"},{code:"ES",name:"Spain"},{code:"LK",name:"Sri Lanka"},{code:"SD",name:"Sudan"},{code:"SR",name:"Suriname"},{code:"SE",name:"Sweden"},{code:"CH",name:"Switzerland"},{code:"SY",name:"Syria"},{code:"TW",name:"Taiwan"},{code:"TJ",name:"Tajikistan"},{code:"TZ",name:"Tanzania"},{code:"TH",name:"Thailand"},{code:"TL",name:"Timor-Leste"},{code:"TG",name:"Togo"},{code:"TO",name:"Tonga"},{code:"TT",name:"Trinidad and Tobago"},{code:"TN",name:"Tunisia"},{code:"TR",name:"Turkey"},{code:"TM",name:"Turkmenistan"},{code:"TC",name:"Turks and Caicos Islands"},{code:"TV",name:"Tuvalu"},{code:"UG",name:"Uganda"},{code:"UA",name:"Ukraine"},{code:"AE",name:"United Arab Emirates"},{code:"GB",name:"United Kingdom"},{code:"US",name:"United States"},{code:"UY",name:"Uruguay"},{code:"UZ",name:"Uzbekistan"},{code:"VU",name:"Vanuatu"},{code:"VE",name:"Venezuela"},{code:"VN",name:"Vietnam"},{code:"VG",name:"Virgin Islands, British"},{code:"VI",name:"Virgin Islands, U.S."},{code:"YE",name:"Yemen"},{code:"ZM",name:"Zambia"},{code:"ZW",name:"Zimbabwe"}];let l=null;async function d(e){if("proxy"===e.mode){const n=await async function(e){if(l)return l;try{const n=await fetch(`/apps/${e}/auth/token`,{method:"GET",headers:{Accept:"application/json"}}),t=await n.json().catch(()=>null);return n.ok&&t&&t.success&&t.data&&t.data.access_token?(l=t.data.access_token,l):null}catch(e){return console.error("Error fetching proxy access token:",e),null}}(e.proxyApp);return n?`Bearer ${n}`:null}return e.accessToken?/^Bearer /i.test(e.accessToken)?e.accessToken:`Bearer ${e.accessToken}`:(console.error('notify-me-wl: data-auth="token" requires an access token. Set data-access-token on the open button or window.TWC_ACCESS_TOKEN, or use data-auth="proxy" to obtain one via the Shopify App Proxy.'),null)}const u=new Map;function p(e){return"string"==typeof e?e.trim():""}async function w(e,n,t){const o=`${e}|${t||""}`,a=u.get(o);if(void 0!==a)return a;const r=await d(n);if(!r)return null;try{const n=await fetch("https://api.au-aws.thewishlist.io/services/eventcollector/api/v1/custom/configs/public",{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":e}});if(!n.ok)return console.error("Tenant config request failed:",n.status),null;const a=function(e,n=null){if(!e||"object"!=typeof e)return null;const t=e.websiteRecommendations;if(!t||"object"!=typeof t)return null;const o=p(t.engine).toUpperCase();if("TWC"===o)return{engine:"TWC"};if("ATHOS"===o){const e=p(t.siteIdentifier);return e?{engine:"ATHOS",siteIdentifier:e,profileTag:p(t.profileTag)||p(t.tags)||p(n)||"similar"}:null}return null}(await n.json().catch(()=>null),t);return u.set(o,a),a}catch(e){return console.error("Error fetching tenant config:",e),null}}function f(e){return"string"==typeof e?e.trim():""}function y(e){if("number"==typeof e&&Number.isFinite(e))return e;if("string"==typeof e){const n=parseFloat(e);if(Number.isFinite(n))return n}return null}function h(e){return Boolean(e.name&&e.imageUrl&&e.productUrl)}function g(e,n){const t=new Map;return e.forEach(e=>{const n=t.get(e.product.ref);n&&n.score>=e.score||t.set(e.product.ref,e)}),Array.from(t.values()).sort((e,n)=>n.score-e.score).slice(0,n).map(e=>e.product)}async function v(e){const{email:n,tenant:t,auth:o,count:a}=e,r=await d(o);if(!r)return null;const c=Math.min(3*a,20),i=`https://api.au-aws.thewishlist.io/services/recommendations/api/v1/recommendations/${encodeURIComponent(t)}/${encodeURIComponent(n)}?n=${c}`;try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json",Authorization:r,"X-Twc-Tenant":t}});if(!e.ok)return console.error("Recommendations request failed:",e.status),null;return function(e,n){const t=e&&e.recommendations;if(!Array.isArray(t))return[];const o=[];return t.forEach(e=>{var n;const t=e&&e.product;if(!t)return;const a=f(t.name),r=f(t.image_url),c=f(t.product_url);if(!h({name:a,imageUrl:r,productUrl:c}))return;const i=f(t.product_ref)||f(t.product_id)||c;o.push({score:"number"==typeof e.score?e.score:0,product:{id:f(t.product_id)||i,ref:i,name:a,imageUrl:r,productUrl:c,price:null!==(n=y(t.price))&&void 0!==n?n:0,originalPrice:y(t.original_price)}})}),g(o,n)}(await e.json().catch(()=>null),a)}catch(e){return console.error("Error fetching recommendations:",e),null}}async function b(e){const{engine:n,email:t,count:o,productId:a}=e,r=encodeURIComponent(n.siteIdentifier),c=[`tags=${encodeURIComponent(n.profileTag)}`,`limits=${o}`];a&&c.push(`products=${encodeURIComponent(a)}`),t&&c.push(`shopper=${encodeURIComponent(t)}`);const i=`https://${r}.a.searchspring.io/boost/${r}/recommend?`+c.join("&");try{const e=await fetch(i,{method:"GET",headers:{Accept:"application/json"}});if(!e.ok)return console.error("Athos recommendations request failed:",e.status),null;return function(e,n){const t=Array.isArray(e)?e[0]:e,o=t&&t.results;if(!Array.isArray(o))return[];const a=[];return o.forEach((e,n)=>{var t;const r=e&&e.mappings&&e.mappings.core;if(!r)return;const c=f(r.name),i=f(r.imageUrl)||f(r.thumbnailImageUrl),s=f(r.url);if(!h({name:c,imageUrl:i,productUrl:s}))return;const m=f(r.uid)||f(r.sku)||s,l=null!==(t=y(r.price))&&void 0!==t?t:0,d=y(r.msrp);a.push({score:o.length-n,product:{id:f(r.uid)||m,ref:m,name:c,imageUrl:i,productUrl:s,price:l,originalPrice:null!==d&&d>l?d:null}})}),g(a,n)}(await e.json().catch(()=>null),o)}catch(e){return console.error("Error fetching Athos recommendations:",e),null}}const x=new Map;const C="twc-nm-recs",k="twc-nm-recs-title";function S(e,n){if(n)try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n}).format(e)}catch(e){}return new Intl.NumberFormat(void 0,{minimumFractionDigits:0,maximumFractionDigits:2}).format(e)}function E(){const e=document.createElement("section");e.id=C,e.className="twc-nm-recs",e.setAttribute("aria-labelledby",k);const n=document.createElement("h3");n.id=k,n.className="twc-nm-recs-title",n.textContent="Shop similar styles",e.appendChild(n);const t=document.createElement("div");return t.className="twc-nm-recs-row",function(e){let n=0,t=0,o=!1,a=!1,r=!1;e.addEventListener("pointerdown",c=>{"mouse"===c.pointerType&&0===c.button&&(o=!0,a=!1,r=!1,n=c.clientX,t=e.scrollLeft,e.classList.add("twc-nm-recs-row--dragging"))}),e.addEventListener("pointermove",r=>{if(!o)return;const c=r.clientX-n;!a&&Math.abs(c)<5||(a=!0,e.scrollLeft=t-c,r.preventDefault())});const c=()=>{o&&(o=!1,r=a,e.classList.remove("twc-nm-recs-row--dragging"))};e.addEventListener("pointerup",c),e.addEventListener("pointercancel",c),e.addEventListener("pointerleave",c),e.addEventListener("click",e=>{r&&(r=!1,e.preventDefault(),e.stopPropagation())},!0),e.addEventListener("dragstart",e=>{o&&a&&e.preventDefault()})}(t),e.appendChild(t),{section:e,row:t}}function A(){const e=document.createElement("div");e.className="twc-nm-rec twc-nm-rec--skeleton",e.setAttribute("aria-hidden","true");const n=document.createElement("div");n.className="twc-nm-rec-imgbox twc-nm-skel",e.appendChild(n);const t=document.createElement("span");t.className="twc-nm-skel twc-nm-skel-line",e.appendChild(t);const o=document.createElement("span");return o.className="twc-nm-skel twc-nm-skel-line twc-nm-skel-line--short",e.appendChild(o),e}function L(e,n,t){const o=e.querySelector(".twc-nm-recs-row");if(!o)return;const a=(r=t,window.Shopify&&window.Shopify.currency&&window.Shopify.currency.active||r||null);var r;const c=document.createDocumentFragment();n.forEach(e=>{c.appendChild(function(e,n){const t=document.createElement("a");t.className="twc-nm-rec",t.href=e.productUrl,t.target="_blank",t.rel="noopener noreferrer";const o=document.createElement("div");o.className="twc-nm-rec-imgbox";const a=document.createElement("img");a.className="twc-nm-rec-img",a.src=e.imageUrl,a.alt=e.name,a.loading="lazy",a.decoding="async",o.appendChild(a),t.appendChild(o);const r=document.createElement("span");r.className="twc-nm-rec-name",r.textContent=e.name,t.appendChild(r);const c=document.createElement("span");if(c.className="twc-nm-rec-price",null!==e.originalPrice&&e.originalPrice>e.price){const t=document.createElement("s");t.className="twc-nm-rec-was",t.textContent=S(e.originalPrice,n),c.appendChild(t),c.appendChild(document.createTextNode(" "))}return c.appendChild(document.createTextNode(S(e.price,n))),t.appendChild(c),t}(e,a))}),o.textContent="",o.appendChild(c),e.classList.remove("twc-nm-recs--loading"),e.removeAttribute("aria-busy")}function T(){const e=document.getElementById(C);e&&e.parentNode&&e.parentNode.removeChild(e)}const M={"notify-me":{intro:"Register to receive a notification as soon as this item is back in stock",submit:"Notify Me"},"coming-soon":{intro:"Register your interest to hear more about this item",submit:"Register Interest"}},N=["title","intro","sizeLabel","sizePlaceholder","emailPlaceholder","emailOptIn","smsOptIn","submit","success","disclaimer"];function I(e,n){const t={title:"{product}",intro:M[n].intro,sizeLabel:"Size",sizePlaceholder:"Select a size",emailLabel:"Email",emailPlaceholder:"you@example.com",emailOptIn:"Email me store updates",smsOptIn:"Text me store updates",submit:M[n].submit,success:"You're on the list. We'll be in touch.",disclaimer:null};if(void 0===e)return t;if(!e||"object"!=typeof e||Array.isArray(e))return a("copy","an object"),t;const o=e;N.forEach(e=>{const n=o[e];null!=n&&("string"==typeof n?t[e]=n:a(`copy.${e}`,"a string"))});const r=o.emailLabel;return!1===r||"string"==typeof r?t.emailLabel=r:null!=r&&a("copy.emailLabel","a string or false"),t}function B(e,n){return e.split("{product}").join(n)}function P(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function $(e){if("string"!=typeof e)return null;const n=e.trim();return/^(https?:)?\/\//i.test(n)?n:null}function z(e){return"string"==typeof e?e.trim():""}function G(o){const{wrapper:a,config:r,localization:i}=o,{fields:s,type:l,display:u,auth:p,tenant:f,recommendationsEnabled:y,recommendationsCount:h,recommendationsProfile:g,currency:k,customerEmail:S,copy:M,productSummary:N,classes:I,jsonConfigured:G}=r,U='<button type="button" id="popup-close" class="twc-nm-close" aria-label="Close"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" fill="none"/></svg></button>',j='<h2 id="popup-title" class="twc-nm-title"></h2>',R=G?`<div class="twc-nm-header">${U}${j}</div>`:`${U}\n ${j}`,O=document.createElement("div");O.innerHTML=`\n <div id="twc-nm-overlay" role="dialog" aria-modal="true" aria-labelledby="popup-title">\n <div class="twc-nm-card" tabindex="-1">\n ${R}\n <p id="popup-text" class="twc-nm-text"></p>\n <div id="popup-notice" class="twc-nm-status" role="status" aria-live="polite" hidden></div>\n <form id="popup-form" class="twc-nm-form">\n <div class="twc-nm-field">\n <label class="twc-nm-label" for="twc-nm-size">${P(M.sizeLabel)}</label>\n <select class="twc-nm-select" id="twc-nm-size" name="select-size" required>\n <option value="" disabled selected>${P(M.sizePlaceholder)}</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n `,a.appendChild(O);const H=document.getElementById("twc-nm-overlay"),F=document.getElementById("popup-close"),K=document.getElementById("popup-title"),D=document.getElementById("popup-text"),q=document.querySelector("select[name='select-size']"),_=document.getElementById("popup-form"),V=document.getElementById("popup-notice");if(!(H&&F&&K&&D&&q&&_&&V))return null;"panel"===u&&H.classList.add("twc-nm--panel");const J=N?function(){const e=document.createElement("div");return e.className="twc-nm-summary",e.innerHTML='<div class="twc-nm-summary-image"><img alt="" /></div><div class="twc-nm-summary-info"><p class="twc-nm-summary-name"></p><p class="twc-nm-summary-variant"></p><p class="twc-nm-summary-price"><span class="twc-nm-summary-now"></span> <s class="twc-nm-summary-compare"></s></p></div>',e}():null;J&&_.insertBefore(J,_.firstChild);const Z=m.map(e=>`<option value="${e.code}"${e.code===i.countryCode?" selected":""}>${e.name}</option>`).join(""),W=(e,n,t)=>`<div class="twc-nm-field"><label class="twc-nm-label" for="${e}">${n}</label>${t}</div>`,Y={email:!1===M.emailLabel?`<div class="twc-nm-field"><input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(M.emailPlaceholder)}" aria-label="${P(M.emailPlaceholder||"Email")}" required /></div>`:W("twc-nm-email",P(M.emailLabel),`<input class="twc-nm-input" id="twc-nm-email" name="email" type="email" placeholder="${P(M.emailPlaceholder)}" required />`),mobile:W("twc-nm-mobile","Mobile",'<input class="twc-nm-input" id="twc-nm-mobile" name="mobile" type="tel" placeholder="Mobile number" required />'),firstName:W("twc-nm-firstname","First name",'<input class="twc-nm-input" id="twc-nm-firstname" name="firstName" type="text" placeholder="First name" required />'),lastName:W("twc-nm-lastname","Last name",'<input class="twc-nm-input" id="twc-nm-lastname" name="lastName" type="text" placeholder="Last name" required />'),countryCode:W("twc-nm-country","Country",`<select class="twc-nm-select" id="twc-nm-country" name="countryCode"><option value="">Select country</option>${Z}</select>`),provinceCode:W("twc-nm-province","State / Province",`<input class="twc-nm-input" id="twc-nm-province" name="provinceCode" type="text" placeholder="State / province code" value="${i.provinceCode||""}" />`)};s.forEach(e=>{Y[e]&&_.insertAdjacentHTML("beforeend",Y[e])});const X=[s.includes("email")?`<label class="twc-nm-check"><input type="checkbox" name="mailList" /> ${P(M.emailOptIn)}</label>`:"",s.includes("mobile")?`<label class="twc-nm-check"><input type="checkbox" name="mailListSms" /> ${P(M.smsOptIn)}</label>`:""].join("");if(X&&_.insertAdjacentHTML("beforeend",`<div class="twc-nm-checks">${X}</div>`),_.insertAdjacentHTML("beforeend",'<div id="popup-status" class="twc-nm-status" role="status" aria-live="polite" hidden></div>'),_.insertAdjacentHTML("beforeend",`<button type="submit" class="twc-nm-submit">${P(M.submit)}</button>`),M.disclaimer&&_.insertAdjacentHTML("beforeend",`<p class="twc-nm-disclaimer">${P(M.disclaimer)}</p>`),function(e,n){Object.keys(n).forEach(t=>{const o=n[t];o&&e.querySelectorAll(c[t]).forEach(e=>{e.classList.add(...o,"twc-nm-themed")})})}(H,I),S){const e=_.querySelector("input[name='email']");e&&(e.value=S)}const Q=document.getElementById("popup-status"),ee=_.querySelector(".twc-nm-submit");if(!Q||!ee)return null;function ne(e,n){Q.textContent=n,Q.className=`twc-nm-status twc-nm-status--${e}`,Q.hidden=!1}function te(){Q.hidden=!0,Q.textContent=""}function oe(e,n){_.hidden=!0,V.textContent=n,V.className=`twc-nm-status twc-nm-status--${e}`,V.hidden=!1}const ae=e=>{const n=_.querySelector(e);return n?n.value:""},re=e=>{const n=_.querySelector(e);return!!n&&n.checked},ce=H.querySelector(".twc-nm-card");let ie=null,se=[],me=null;async function le(e){const n=await function(e,n,t){return w(e,n,t)}(f,p,g);if(!n)return!1;if(!H.classList.contains("is-open"))return!1;const t=function(e){const{section:n,row:t}=E();n.classList.add("twc-nm-recs--loading"),n.setAttribute("aria-busy","true");for(let n=0;n<e;n++)t.appendChild(A());return n}(h);ce.appendChild(t);const o=await async function(e,n){const{email:t,tenant:o,auth:a,count:r,productId:c}=n,i=`${e.engine}|${o}|${t}|${r}|${c||""}`,s=x.get(i);if(s)return s;let m=null;return m="ATHOS"===e.engine?await b({engine:e,email:t,count:r,productId:c}):await v({email:t,tenant:o,auth:a,count:r}),null===m?[]:(x.set(i,m),m)}(n,{email:e,tenant:f,auth:p,count:h,productId:ie?String(ie.id):null});return!!t.isConnected&&(o.length?(L(t,o,k),!0):(T(),!1))}function de(e){if(!y||!ce)return Promise.resolve(!1);if(me)return me;if(document.getElementById(C))return Promise.resolve(!0);const n=le(e);return me=n,n.then(e=>(me===n&&(me=null),e),e=>(me===n&&(me=null),console.error("Error rendering recommendations:",e),!1))}let ue=null,pe=0;function we(e){"Escape"===e.key&&fe()}function fe(){pe++,H.classList.remove("is-open"),document.body.style.overflow="",document.removeEventListener("keydown",we),te(),T(),me=null,ue&&"function"==typeof ue.focus&&ue.focus()}function ye(){if(!J||!ie)return;const e=se.find(e=>String(e.id)===q.value)||null;!function(e,n,t,o){const a=n=>e.querySelector(n),r=a(".twc-nm-summary-image"),c=a(".twc-nm-summary-image img"),i=t&&$(t.image)||$(n.image);i?(c.src=i,c.alt=n.title):c.removeAttribute("src"),r.hidden=!i,a(".twc-nm-summary-name").textContent=n.title;const s=a(".twc-nm-summary-variant");s.textContent=t?t.title:"",s.hidden=!t;let m="",l="";if(t)m=z(t.price),l=z(t.compareAtPrice);else{const e=o.map(e=>z(e.price));e.length&&e[0]&&e.every(n=>n===e[0])&&(m=e[0])}l===m&&(l=""),a(".twc-nm-summary-now").textContent=m;const d=a(".twc-nm-summary-compare");d.textContent=l,d.hidden=!l,a(".twc-nm-summary-price").hidden=!m}(J,ie,e,se)}return F.addEventListener("click",fe),q.addEventListener("change",ye),H.addEventListener("click",e=>{e.target===H&&fe()}),_.addEventListener("submit",async e=>{e.preventDefault();const n=se.find(e=>String(e.id)===q.value);if(!n)return void ne("error","Please select a size.");const t={variantRef:n.id,email:ae("input[name='email']"),subscribe:re("input[name='mailList']"),subscribeSms:re("input[name='mailListSms']")};if("coming-soon"===l?t.comingSoon=!0:t.notifyMe=!0,s.includes("firstName")&&(t.firstName=ae("input[name='firstName']")),s.includes("lastName")&&(t.lastName=ae("input[name='lastName']")),s.includes("mobile")&&(t.mobile=ae("input[name='mobile']")),s.includes("countryCode")){const e=ae("select[name='countryCode']");e&&(t.countryCode=e)}else i.countryCode&&(t.countryCode=i.countryCode);if(s.includes("provinceCode")){const e=ae("input[name='provinceCode']");e&&(t.provinceCode=e)}else i.provinceCode&&(t.provinceCode=i.provinceCode);i.marketId&&(t.marketId=i.marketId),i.marketHandle&&(t.marketHandle=i.marketHandle),te();const o=pe,a=ee.textContent||"";ee.disabled=!0,ee.textContent="Sending…";let r={ok:!1,reason:"error"};try{r=await async function(e,n,t){const o=await d(n);if(!o)return{ok:!1,reason:"auth"};try{return(await fetch("https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest",{method:"POST",headers:{"Content-Type":"application/json",Authorization:o,"X-Twc-Tenant":t},body:JSON.stringify(e)})).ok?{ok:!0}:{ok:!1,reason:"error"}}catch(e){return console.error("Error submitting form:",e),{ok:!1,reason:"error"}}}(t,p,f)}finally{o===pe&&(ee.disabled=!1,ee.textContent=a)}if(o===pe)if(r.ok){ne("success",M.success);const e=S||ae("input[name='email']");!!e&&await de(e)||o!==pe||setTimeout(()=>{o===pe&&fe()},1500)}else"auth"===r.reason?ne("error","proxy"===p.mode?"Please log in to your account to continue.":"Something went wrong. Please try again."):ne("error","Something went wrong. Please try again.")}),{open:function(o){pe++,ee.disabled=!1,ee.textContent=M.submit,te(),_.hidden=!1,V.hidden=!0,V.textContent="",T(),me=null,H.classList.contains("is-open")||(ue=document.activeElement),H.classList.add("is-open"),document.body.style.overflow="hidden",document.addEventListener("keydown",we);const a=function(t){const o=t?t.closest(`[${e}]`):null;if(o){try{const t=JSON.parse(o.getAttribute(e)||"");if(n(t))return{ok:!0,product:t,source:"card"}}catch(e){}return console.error(`notify-me-wl: invalid ${e} JSON on`,o),{ok:!1}}const a=window.currentProduct;return n(a)?{ok:!0,product:a,source:"page"}:{ok:!1}}(o);if(!a.ok)return ie=null,se=[],K.textContent="",D.textContent=B(M.intro,""),void oe("error","Product information is unavailable. Please refresh the page.");if(ie=a.product,se=(ie.variants||[]).filter(e=>!!e&&("number"==typeof e.id||"string"==typeof e.id)&&"string"==typeof e.title&&!0!==e.available),K.textContent=B(M.title,ie.title),D.textContent=B(M.intro,ie.title),!se.length)return void oe("info","All sizes are currently in stock.");const r=o?o.getAttribute("data-variant-id"):null,c="page"===a.source?[r,...t()]:[r];var i,s;if(i=se,s=function(e,n){const t=e.map(e=>String(e.id));for(const e of n){const n=e?e.trim():"";if(n&&t.includes(n))return n}return 1===t.length?t[0]:null}(se,c),q.innerHTML=`<option value="" disabled selected>${P(M.sizePlaceholder)}</option>`,i.forEach(e=>{const n=document.createElement("option");n.value=String(e.id),n.textContent=e.title,q.appendChild(n)}),s&&(q.value=s),ye(),S&&de(S),"function"==typeof window.matchMedia&&window.matchMedia("(pointer: coarse)").matches)null==ce||ce.focus({preventScroll:!0});else{const e=_.querySelector("select, input");e&&e.focus()}}}}function U(e){var n;const t=(null!=e?e:"").trim();if(!t)return null;const o=null!==(n=t.split("?")[0].split("/").pop())&&void 0!==n?n:"";return/^\d+$/.test(o)?o:null}function j(e){const n=(null!=e?e:"").trim();return/^[a-z0-9_-]+$/i.test(n)?n:null}function R(e){const n=(null!=e?e:"").trim();return/^[a-z]{2}$/i.test(n)?n.toUpperCase():null}function O(e){var n,t,o;const a={countryCode:R(e("data-country-code")),provinceCode:null,marketId:U(e("data-market-id")),marketHandle:j(e("data-market-handle"))},r=window.Shopify;if(r){const e=null===(n=r.markets)||void 0===n?void 0:n.currentMarket;if(e&&(!a.countryCode&&e.countryCode&&(a.countryCode=e.countryCode),a.marketId||(a.marketId=U(String(null!==(t=e.id)&&void 0!==t?t:"")))),a.countryCode||"string"!=typeof r.country||(a.countryCode=r.country),!a.countryCode&&"string"==typeof r.locale){const e=r.locale.split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}}if(!a.countryCode||!a.provinceCode){const e=null===(o=window.customer)||void 0===o?void 0:o.default_address;e&&(!a.countryCode&&e.country_code&&(a.countryCode=e.country_code),!a.provinceCode&&e.province_code&&(a.provinceCode=e.province_code))}if(!a.countryCode){const e=(navigator.language||"").split("-");e.length>1&&(a.countryCode=e[e.length-1].toUpperCase())}return a}function H(e){const n=e("data-customer-email");if(n&&n.trim())return n.trim();const t=window.customer&&window.customer.email;if(t&&t.trim())return t.trim();const o=window.customerEmail;return"string"==typeof o&&o.trim()?o.trim():null}const F="[data-twc-notify-me]";function K(){const e=document.getElementById("notification-widget");if(!e)return;const n=document.getElementById("popup-open"),t=function(e){const n=e.querySelector('script[type="application/json"][data-twc-config]');if(!n)return null;try{const e=JSON.parse(n.textContent||"");if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch(e){}return console.error("notify-me-wl: invalid data-twc-config JSON"),null}(e),c=function(e,n,t){const c=t=>(e?e.getAttribute(t):null)||n.getAttribute(t)||null,i=e=>null===t[e]?void 0:t[e],s=e=>{const n=i(e);return void 0===n?null:"boolean"==typeof n?n:(a(e,"true or false"),null)};return{string(e){const n=c(e);if(null!==n)return n;const t=o(e),r=i(t);return void 0===r?null:"string"==typeof r?r:"number"==typeof r&&Number.isFinite(r)?String(r):(a(t,"a string"),null)},boolean(e){const n=c(e);return null!==n?"false"!==n:s(o(e))},number(e){const n=c(e);if(null!==n){const e=parseInt(n,10);if(Number.isFinite(e))return e}const t=o(e),r=i(t);return void 0===r?null:"number"==typeof r&&Number.isFinite(r)?r:(a(t,"a number"),null)},stringArray(e){const n=c(e);if(null!==n){try{const e=JSON.parse(n);if(r(e))return e}catch(e){}console.warn(`notify-me-wl: ignoring invalid ${e} attribute`)}const t=o(e),s=i(t);return void 0===s?null:r(s)?s:(a(t,"an array of strings"),null)},json:i,jsonBoolean:s}}(n,e,t||{}),i=c.string;!function(){const e=document.createElement("style");e.type="text/css",e.innerText="\n .notification-btn {\n display: inline-block;\n width: 100%;\n padding: 12px 24px;\n background: #fff;\n border: 1px solid #111827;\n border-radius: 8px;\n color: #111827;\n font: inherit;\n text-align: center;\n text-transform: uppercase;\n cursor: pointer;\n }\n .notification-btn:hover {\n background: #111827;\n color: #fff;\n }\n #twc-nm-overlay {\n position: fixed;\n inset: 0;\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: var(--twc-nm-overlay-bg, rgba(17, 17, 17, 0.55));\n opacity: 0;\n visibility: hidden;\n transition: opacity var(--twc-nm-transition, 200ms ease);\n }\n #twc-nm-overlay.is-open {\n opacity: 1;\n visibility: visible;\n }\n #twc-nm-overlay * {\n box-sizing: border-box;\n }\n #twc-nm-overlay .twc-nm-card {\n position: relative;\n width: 100%;\n max-width: 400px;\n max-height: calc(100vh - 32px);\n overflow-y: auto;\n padding: var(--twc-nm-padding, 24px);\n background: var(--twc-nm-bg, #fff);\n color: var(--twc-nm-text, #111827);\n border-radius: 12px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);\n font: inherit;\n font-family: var(--twc-nm-font, inherit);\n transform: translateY(8px);\n transition: transform var(--twc-nm-transition, 200ms ease);\n }\n /* Focused on open on touch devices; it is a container, not a control. */\n #twc-nm-overlay .twc-nm-card:focus {\n outline: none;\n }\n #twc-nm-overlay.is-open .twc-nm-card {\n transform: translateY(0);\n }\n /* Panel mode: same blocking backdrop, docked to the right edge and\n full height instead of centred. Declared after the modal rules so\n the shared properties are overridden. */\n #twc-nm-overlay.twc-nm--panel {\n align-items: stretch;\n justify-content: flex-end;\n padding: 0;\n }\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width, 420px);\n max-width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: var(--twc-nm-panel-shadow, -10px 0 40px rgba(0, 0, 0, 0.18));\n transform: translateX(100%);\n }\n /* Needs the extra class to beat both the .is-open rule above and the\n panel's own resting transform. */\n #twc-nm-overlay.twc-nm--panel.is-open .twc-nm-card {\n transform: translateX(0);\n }\n @media (max-width: 480px) {\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n width: var(--twc-nm-panel-width-mobile, 100%);\n }\n }\n /* Groups the title and close button so a theme can lay them out as a\n row. With no border it changes nothing: the close button stays\n positioned against the card. */\n #twc-nm-overlay .twc-nm-header {\n border-bottom: var(--twc-nm-header-border, none);\n }\n /* Layout below is never themed — it keeps the close button usable\n whatever classes a theme attaches. Cosmetics follow :not(themed). */\n #twc-nm-overlay .twc-nm-close {\n position: absolute;\n top: 12px;\n right: 12px;\n width: 32px;\n height: 32px;\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n border: none;\n background: transparent;\n box-shadow: none;\n text-indent: 0;\n line-height: 1;\n opacity: 1;\n visibility: visible;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)) {\n border-radius: 6px;\n color: var(--twc-nm-muted, #6b7280);\n }\n /* The icon is an inline SVG rather than a text glyph so host-theme\n button resets (font-size: 0, icon fonts, text-indent) can't hide it. */\n #twc-nm-overlay .twc-nm-close svg {\n display: block;\n width: 16px;\n height: 16px;\n flex: none;\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-close::before,\n #twc-nm-overlay .twc-nm-close::after {\n content: none;\n }\n #twc-nm-overlay .twc-nm-close:where(:not(.twc-nm-themed)):hover {\n background: #f3f4f6;\n color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-title {\n margin: 0 32px 4px 0;\n }\n #twc-nm-overlay .twc-nm-title:where(:not(.twc-nm-themed)) {\n font-size: 18px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-text {\n margin: 0 0 16px;\n }\n #twc-nm-overlay .twc-nm-text:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n #twc-nm-overlay .twc-nm-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n #twc-nm-overlay .twc-nm-label:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n font-weight: 500;\n color: #374151;\n }\n #twc-nm-overlay .twc-nm-input,\n #twc-nm-overlay .twc-nm-select {\n width: 100%;\n }\n /* The shorthand resets every background sub-property, so host CSS\n can't leak into an unconfigured widget; the select's arrow is added\n by the rule after it. */\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)),\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n color: var(--twc-nm-text, #111827);\n background: #fff;\n border: 1px solid var(--twc-nm-border, #e5e7eb);\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-input:where(:not(.twc-nm-themed)):focus,\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)):focus {\n outline: none;\n border-color: var(--twc-nm-text, #111827);\n box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);\n }\n #twc-nm-overlay .twc-nm-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n }\n /* After the shared padding above, which it must override. */\n #twc-nm-overlay .twc-nm-select:where(:not(.twc-nm-themed)) {\n padding-right: 36px;\n }\n #twc-nm-overlay .twc-nm-checks {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n #twc-nm-overlay .twc-nm-check {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #374151;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-check input {\n width: 16px;\n height: 16px;\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-check input:where(:not(.twc-nm-themed)) {\n accent-color: var(--twc-nm-text, #111827);\n }\n #twc-nm-overlay .twc-nm-status {\n font-size: 13px;\n line-height: 1.5;\n padding: 10px 12px;\n border-radius: var(--twc-nm-radius, 8px);\n }\n #twc-nm-overlay .twc-nm-status--error {\n color: #b91c1c;\n background: #fef2f2;\n }\n #twc-nm-overlay .twc-nm-status--success {\n color: #15803d;\n background: #f0fdf4;\n }\n #twc-nm-overlay .twc-nm-status--info {\n color: #374151;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-submit {\n width: 100%;\n cursor: pointer;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)) {\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n font-family: inherit;\n color: #fff;\n background: #111827;\n border: none;\n border-radius: var(--twc-nm-radius, 8px);\n transition: background 150ms ease;\n }\n #twc-nm-overlay .twc-nm-submit:where(:not(.twc-nm-themed)):hover {\n background: #374151;\n }\n #twc-nm-overlay .twc-nm-submit:disabled {\n opacity: 0.6;\n cursor: default;\n }\n #twc-nm-overlay .twc-nm-disclaimer {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-disclaimer:where(:not(.twc-nm-themed)) {\n font-size: 12px;\n line-height: 1.5;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary {\n display: flex;\n align-items: flex-start;\n }\n #twc-nm-overlay .twc-nm-summary:where(:not(.twc-nm-themed)) {\n gap: 16px;\n }\n /* Theme classes often set display; hidden parts must stay hidden. */\n #twc-nm-overlay .twc-nm-summary [hidden] {\n display: none !important;\n }\n #twc-nm-overlay .twc-nm-summary-image {\n flex: none;\n }\n #twc-nm-overlay .twc-nm-summary-image:where(:not(.twc-nm-themed)) {\n width: 96px;\n }\n #twc-nm-overlay .twc-nm-summary-image img {\n display: block;\n width: 100%;\n height: auto;\n }\n #twc-nm-overlay .twc-nm-summary-info {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n #twc-nm-overlay .twc-nm-summary-info p {\n margin: 0;\n }\n #twc-nm-overlay .twc-nm-summary-name {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n #twc-nm-overlay .twc-nm-summary-name:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n font-weight: 600;\n }\n #twc-nm-overlay .twc-nm-summary-variant:where(:not(.twc-nm-themed)) {\n font-size: 13px;\n color: var(--twc-nm-muted, #6b7280);\n }\n #twc-nm-overlay .twc-nm-summary-price:where(:not(.twc-nm-themed)) {\n font-size: 14px;\n }\n #twc-nm-overlay .twc-nm-summary-compare:where(:not(.twc-nm-themed)) {\n margin-left: 6px;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-recs {\n margin-top: 20px;\n padding-top: 20px;\n border-top: 1px solid var(--twc-nm-border, #e5e7eb);\n }\n #twc-nm-overlay .twc-nm-recs-title {\n margin: 0 0 12px;\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-recs-row {\n display: flex;\n gap: 12px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 8px;\n scrollbar-width: thin;\n }\n /* Only where a real pointer exists — the affordance is meaningless\n on touch, which pans the row natively. */\n @media (hover: hover) {\n #twc-nm-overlay .twc-nm-recs-row {\n cursor: grab;\n }\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging,\n #twc-nm-overlay .twc-nm-recs-row--dragging .twc-nm-rec {\n cursor: grabbing;\n }\n #twc-nm-overlay .twc-nm-recs-row--dragging {\n /* Snapping fights a manually driven scrollLeft and feels sticky. */\n scroll-snap-type: none;\n user-select: none;\n -webkit-user-select: none;\n }\n #twc-nm-overlay .twc-nm-rec {\n flex: 0 0 132px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 6px;\n color: inherit;\n text-decoration: none;\n }\n #twc-nm-overlay .twc-nm-rec:focus-visible {\n outline: 2px solid #111827;\n outline-offset: 2px;\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox {\n aspect-ratio: 3 / 4;\n overflow: hidden;\n border-radius: 8px;\n background: #f3f4f6;\n }\n #twc-nm-overlay .twc-nm-rec-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 150ms ease;\n }\n #twc-nm-overlay .twc-nm-rec:hover .twc-nm-rec-img {\n opacity: 0.85;\n }\n #twc-nm-overlay .twc-nm-rec-name {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n font-size: 13px;\n line-height: 1.35;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-price {\n font-size: 13px;\n font-weight: 600;\n color: #111827;\n }\n #twc-nm-overlay .twc-nm-rec-was {\n margin-right: 4px;\n font-weight: 400;\n color: #9ca3af;\n }\n #twc-nm-overlay .twc-nm-rec--skeleton {\n pointer-events: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n background-color: #f3f4f6;\n background-image: linear-gradient(\n 90deg,\n #f3f4f6 0%,\n #e8eaee 50%,\n #f3f4f6 100%\n );\n background-size: 200% 100%;\n border-radius: 4px;\n animation: twc-nm-shimmer 1.3s ease-in-out infinite;\n }\n #twc-nm-overlay .twc-nm-rec-imgbox.twc-nm-skel {\n border-radius: 8px;\n }\n #twc-nm-overlay .twc-nm-skel-line {\n height: 12px;\n width: 100%;\n }\n #twc-nm-overlay .twc-nm-skel-line--short {\n width: 45%;\n }\n @keyframes twc-nm-shimmer {\n from {\n background-position: 200% 0;\n }\n to {\n background-position: -200% 0;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n #twc-nm-overlay,\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay .twc-nm-rec-img {\n transition: none;\n }\n #twc-nm-overlay .twc-nm-card,\n #twc-nm-overlay.twc-nm--panel .twc-nm-card {\n transform: none;\n }\n #twc-nm-overlay .twc-nm-skel {\n animation: none;\n }\n }\n ",document.head.appendChild(e)}();const m=c.stringArray("data-fields")||["email"],l=i("data-type")||"notify-me",d="panel"===i("data-display")?"panel":"modal",u=i("data-auth")||"proxy",p=i("data-tenant")||"victoria-woods",w=i("data-proxy-app")||"twc-sdk",f=i("data-access-token")||window.TWC_ACCESS_TOKEN||null,y=!1!==c.boolean("data-recommendations"),h=c.number("data-recommendations-count"),g=G({wrapper:e,config:{fields:m,type:l,display:d,auth:{mode:u,proxyApp:w,accessToken:f},tenant:p,recommendationsEnabled:y,recommendationsCount:null!==h&&h>0?h:8,recommendationsProfile:(i("data-recommendations-profile")||"").trim()||null,currency:i("data-currency")||null,customerEmail:H(i),copy:I(c.json("copy"),l),productSummary:!0===c.jsonBoolean("productSummary"),classes:s(c.json("classes")),jsonConfigured:null!==t},localization:O(i)});g&&(document.addEventListener("click",e=>{const n=e.target,t=n instanceof Element?n.closest(F):null;t&&(e.preventDefault(),g.open(t))},!0),n&&!n.matches(F)&&n.addEventListener("click",()=>g.open(n)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",K):K()}();
|